From 66fed00eb2ee55d7f00dd238bce45018049c9129 Mon Sep 17 00:00:00 2001 From: Iwaniukooo11 Date: Tue, 30 Jun 2026 13:13:50 +0000 Subject: [PATCH 1/2] feat(signing-and-verifying): Add go sample --- .../agents/signing_and_verifying/.gitignore | 3 + .../signing_and_verifying/Containerfile | 20 ++ .../go/agents/signing_and_verifying/README.md | 64 ++++++ .../signing_and_verifying/agent_executor.go | 37 ++++ .../go/agents/signing_and_verifying/go.mod | 15 ++ .../go/agents/signing_and_verifying/go.sum | 20 ++ .../go/agents/signing_and_verifying/main.go | 164 ++++++++++++++++ .../agents/signing_and_verifying/signing.go | 183 ++++++++++++++++++ .../signing_and_verifying/test_client.go | 107 ++++++++++ 9 files changed, 613 insertions(+) create mode 100644 samples/go/agents/signing_and_verifying/.gitignore create mode 100644 samples/go/agents/signing_and_verifying/Containerfile create mode 100644 samples/go/agents/signing_and_verifying/README.md create mode 100644 samples/go/agents/signing_and_verifying/agent_executor.go create mode 100644 samples/go/agents/signing_and_verifying/go.mod create mode 100644 samples/go/agents/signing_and_verifying/go.sum create mode 100644 samples/go/agents/signing_and_verifying/main.go create mode 100644 samples/go/agents/signing_and_verifying/signing.go create mode 100644 samples/go/agents/signing_and_verifying/test_client.go diff --git a/samples/go/agents/signing_and_verifying/.gitignore b/samples/go/agents/signing_and_verifying/.gitignore new file mode 100644 index 000000000..456ced1d0 --- /dev/null +++ b/samples/go/agents/signing_and_verifying/.gitignore @@ -0,0 +1,3 @@ +# File generated for storing the public key used in verification of AgentCard signature +public_keys.json +signing_and_verifying diff --git a/samples/go/agents/signing_and_verifying/Containerfile b/samples/go/agents/signing_and_verifying/Containerfile new file mode 100644 index 000000000..cb1c19319 --- /dev/null +++ b/samples/go/agents/signing_and_verifying/Containerfile @@ -0,0 +1,20 @@ +FROM golang:1.24-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o agent . + +FROM alpine:latest + +WORKDIR /app + +COPY --from=builder /app/agent . + +EXPOSE 9999 + +CMD ["./agent"] diff --git a/samples/go/agents/signing_and_verifying/README.md b/samples/go/agents/signing_and_verifying/README.md new file mode 100644 index 000000000..14d7f704f --- /dev/null +++ b/samples/go/agents/signing_and_verifying/README.md @@ -0,0 +1,64 @@ +# Signing and Verifying Example (Go) + +Signed agent used as an example for AgentCard signing and verifying in Go. + +Read more about signing and verifying AgentCards here: [Agent Card Signing](https://a2a-protocol.org/latest/specification/#84-agent-card-signing). + +## Getting started + +1. Start the server: + + ```bash + go run . + ``` + +2. Run the test client (in a separate terminal or using the `-client` flag): + + ```bash + go run . -client + ``` + +## Build Container Image + +Agent can also be built using a container file. + +1. Navigate to the `samples/go/agents/signing_and_verifying` directory: + + ```bash + cd samples/go/agents/signing_and_verifying + ``` + +2. Build the container file: + + ```bash + podman build . -t signing_and_verifying-a2a-server + ``` + +> [!Tip] +> Podman is a drop-in replacement for `docker` which can also be used in these commands. + +3. Run your container: + + ```bash + podman run -p 9999:9999 signing_and_verifying-a2a-server + ``` + +## Validate + +To validate in a separate terminal, run the A2A CLI host: + +```bash +cd ../../../python/hosts/cli +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +python3 __main__.py --agent http://localhost:9999 +``` + +## Disclaimer + +Important: The sample code provided is for demonstration purposes and illustrates the mechanics of the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity. + +All data received from an external agent—including but not limited to its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide an AgentCard containing crafted data in its fields (e.g., description, name, skills.description). If this data is used without sanitization to construct prompts for a Large Language Model (LLM), it could expose your application to prompt injection attacks. Failure to properly validate and sanitize this data before use can introduce security vulnerabilities into your application. + +Developers are responsible for implementing appropriate security measures, such as input validation and secure handling of credentials to protect their systems and users. diff --git a/samples/go/agents/signing_and_verifying/agent_executor.go b/samples/go/agents/signing_and_verifying/agent_executor.go new file mode 100644 index 000000000..8929e8970 --- /dev/null +++ b/samples/go/agents/signing_and_verifying/agent_executor.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "fmt" + "iter" + "log" + + "github.com/a2aproject/a2a-go/a2a" + "github.com/a2aproject/a2a-go/a2asrv" +) + +type SignedAgentExecutor struct{} + +func NewSignedAgentExecutor() *SignedAgentExecutor { + return &SignedAgentExecutor{} +} + +func (e *SignedAgentExecutor) Execute(_ context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] { + return func(yield func(a2a.Event, error) bool) { + if execCtx.StoredTask == nil { + if !yield(a2a.NewSubmittedTask(execCtx, execCtx.Message), nil) { + return + } + } + + msg := a2a.NewMessage(a2a.MessageRoleAgent, a2a.NewTextPart("Verify me!")) + yield(a2a.NewStatusUpdateEvent(execCtx, a2a.TaskStateCompleted, msg), nil) + } +} + +func (e *SignedAgentExecutor) Cancel(_ context.Context, _ *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] { + return func(yield func(a2a.Event, error) bool) { + log.Println("Cancel not supported.") + yield(nil, fmt.Errorf("cancel is not supported")) + } +} diff --git a/samples/go/agents/signing_and_verifying/go.mod b/samples/go/agents/signing_and_verifying/go.mod new file mode 100644 index 000000000..498f0bcf8 --- /dev/null +++ b/samples/go/agents/signing_and_verifying/go.mod @@ -0,0 +1,15 @@ +module samples/go/agents/signing_and_verifying + +go 1.24.4 + +require ( + github.com/a2aproject/a2a-go v1.0.0-alpha.3.0.20260309154536-e4af18e0d8c2 + github.com/golang-jwt/jwt/v5 v5.3.1 +) + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/gowebpki/jcs v1.0.1 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/sync v0.15.0 // indirect +) diff --git a/samples/go/agents/signing_and_verifying/go.sum b/samples/go/agents/signing_and_verifying/go.sum new file mode 100644 index 000000000..d8b7770f9 --- /dev/null +++ b/samples/go/agents/signing_and_verifying/go.sum @@ -0,0 +1,20 @@ +github.com/a2aproject/a2a-go v1.0.0-alpha.3.0.20260309154536-e4af18e0d8c2 h1:ngDmlC07rq/kyFUqv2VQn409LsDlSeJDZa9sC93En6g= +github.com/a2aproject/a2a-go v1.0.0-alpha.3.0.20260309154536-e4af18e0d8c2/go.mod h1:Tf22hPDdmrXOLBtHDdhqTIcJVdM6BpB/0ReVC5gUXnM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU= +github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/samples/go/agents/signing_and_verifying/main.go b/samples/go/agents/signing_and_verifying/main.go new file mode 100644 index 000000000..0fc9e1ec8 --- /dev/null +++ b/samples/go/agents/signing_and_verifying/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/json" + "encoding/pem" + "flag" + "log" + "net/http" + "os" + "time" + + "github.com/a2aproject/a2a-go/a2a" + "github.com/a2aproject/a2a-go/a2asrv" +) + +func main() { + runClientFlag := flag.Bool("client", false, "Run the test client instead of starting server only") + flag.Parse() + + // Generate a private, public key pair + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + log.Fatalf("Failed to generate private key: %v", err) + } + publicKey := &privateKey.PublicKey + + // Save public key to a file + pubBytes, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + log.Fatalf("Failed to marshal public key: %v", err) + } + pemBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubBytes, + } + pemStr := string(pem.EncodeToMemory(pemBlock)) + kid := "my-key" + keys := map[string]string{kid: pemStr} + keysJSON, err := json.MarshalIndent(keys, "", " ") + if err != nil { + log.Fatalf("Failed to marshal keys JSON: %v", err) + } + if err := os.WriteFile("public_keys.json", keysJSON, 0644); err != nil { + log.Fatalf("Failed to save public_keys.json: %v", err) + } + + skill := a2a.AgentSkill{ + ID: "reminder", + Name: "Verification Reminder", + Description: "Reminds the user to verify the Agent Card.", + Tags: []string{"verify me"}, + Examples: []string{"Verify me!"}, + } + + extendedSkill := a2a.AgentSkill{ + ID: "reminder-please", + Name: "Verification Reminder Please!", + Description: "Politely reminds user to verify the Agent Card.", + Tags: []string{"verify me", "pretty please", "extended"}, + Examples: []string{"Verify me, pretty please! :)", "Please verify me."}, + } + + publicAgentCard := &a2a.AgentCard{ + Name: "Signed Agent", + Description: "An Agent that is signed", + IconURL: "http://localhost:9999/", + Version: "1.0.0", + DefaultInputModes: []string{"text"}, + DefaultOutputModes: []string{"text"}, + Capabilities: a2a.AgentCapabilities{Streaming: true, ExtendedAgentCard: true}, + SupportedInterfaces: []*a2a.AgentInterface{ + { + ProtocolBinding: a2a.TransportProtocolJSONRPC, + ProtocolVersion: a2a.Version, + URL: "http://localhost:9999", + }, + }, + Skills: []a2a.AgentSkill{skill}, + } + + extendedAgentCard := &a2a.AgentCard{ + Name: "Signed Agent - Extended Edition", + Description: "The full-featured signed agent for authenticated users.", + IconURL: "http://localhost:9999/", + Version: "1.0.1", + DefaultInputModes: []string{"text"}, + DefaultOutputModes: []string{"text"}, + Capabilities: a2a.AgentCapabilities{Streaming: true, ExtendedAgentCard: true}, + SupportedInterfaces: []*a2a.AgentInterface{ + { + ProtocolBinding: a2a.TransportProtocolJSONRPC, + ProtocolVersion: a2a.Version, + URL: "http://localhost:9999", + }, + }, + Skills: []a2a.AgentSkill{ + skill, + extendedSkill, + }, + } + + // Create signer function which will be used for AgentCard signing + signer := createAgentCardSigner( + privateKey, + ProtectedHeader{ + Kid: kid, + Alg: "ES256", + Jku: "http://localhost:9999/public_keys.json", + }, + ) + + signedPublicCard, err := signer(publicAgentCard) + if err != nil { + log.Fatalf("Failed to sign public agent card: %v", err) + } + + signedExtendedCard, err := signer(extendedAgentCard) + if err != nil { + log.Fatalf("Failed to sign extended agent card: %v", err) + } + + requestHandler := a2asrv.NewHandler( + NewSignedAgentExecutor(), + a2asrv.WithExtendedAgentCard(signedExtendedCard), + ) + + mux := http.NewServeMux() + mux.Handle(a2asrv.WellKnownAgentCardPath, a2asrv.NewStaticAgentCardHandler(signedPublicCard)) + mux.Handle("/", a2asrv.NewJSONRPCHandler(requestHandler)) + + // Expose the public key for verification purposes + // Contents of public_keys.json will be fetched on the client side during AgentCard signatures verification + mux.HandleFunc("/public_keys.json", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "public_keys.json") + }) + + server := &http.Server{ + Addr: "127.0.0.1:9999", + Handler: mux, + ReadHeaderTimeout: 3 * time.Second, + } + + if *runClientFlag { + go func() { + log.Println("Starting server on http://127.0.0.1:9999...") + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server failed: %v", err) + } + }() + + time.Sleep(200 * time.Millisecond) + runTestClient() + return + } + + log.Println("Starting server on http://127.0.0.1:9999...") + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server failed: %v", err) + } +} diff --git a/samples/go/agents/signing_and_verifying/signing.go b/samples/go/agents/signing_and_verifying/signing.go new file mode 100644 index 000000000..95773f2e3 --- /dev/null +++ b/samples/go/agents/signing_and_verifying/signing.go @@ -0,0 +1,183 @@ +package main + +import ( + "crypto/ecdsa" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + + "github.com/a2aproject/a2a-go/a2a" + "github.com/golang-jwt/jwt/v5" +) + +// Sentinel errors for signature verification matching Python SDK exception types. +var ( + ErrNoSignature = errors.New("AgentCard has no signatures to verify") + ErrInvalidSignatures = errors.New("No valid signature found") +) + +// ProtectedHeader defines the protected header parameters for JWS (RFC 7515). +type ProtectedHeader struct { + Kid string `json:"kid"` + Alg string `json:"alg,omitempty"` + Jku string `json:"jku,omitempty"` + Typ string `json:"typ,omitempty"` +} + +// KeyProvider is a function type that returns a verification key (e.g., *ecdsa.PublicKey) for a given kid and jku. +type KeyProvider func(kid, jku string) (any, error) + +func cleanEmpty(v any) any { + switch val := v.(type) { + case map[string]any: + cleanedMap := make(map[string]any) + for k, item := range val { + if cleanedItem := cleanEmpty(item); cleanedItem != nil { + cleanedMap[k] = cleanedItem + } + } + if len(cleanedMap) == 0 { + return nil + } + return cleanedMap + case []any: + var cleanedList []any + for _, item := range val { + if cleanedItem := cleanEmpty(item); cleanedItem != nil { + cleanedList = append(cleanedList, cleanedItem) + } + } + if len(cleanedList) == 0 { + return nil + } + return cleanedList + case string: + if val == "" { + return nil + } + return val + default: + return val + } +} + +// canonicalizeAgentCard produces canonical JSON according to RFC 8785 (JCS). +func canonicalizeAgentCard(card *a2a.AgentCard) ([]byte, error) { + data, err := json.Marshal(card) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + delete(raw, "signatures") + + cleaned := cleanEmpty(raw) + if cleaned == nil { + return []byte("{}"), nil + } + return json.Marshal(cleaned) +} + +// createAgentCardSigner creates a function that signs an AgentCard and appends the signature. +func createAgentCardSigner(privateKey *ecdsa.PrivateKey, protectedHeader ProtectedHeader) func(card *a2a.AgentCard) (*a2a.AgentCard, error) { + if protectedHeader.Alg == "" { + protectedHeader.Alg = "ES256" + } + + method := jwt.GetSigningMethod(protectedHeader.Alg) + + return func(card *a2a.AgentCard) (*a2a.AgentCard, error) { + if method == nil { + return nil, fmt.Errorf("unsupported signing algorithm: %s", protectedHeader.Alg) + } + + cardCopy := *card + cardCopy.Signatures = append([]a2a.AgentCardSignature(nil), card.Signatures...) + + canonPayload, err := canonicalizeAgentCard(&cardCopy) + if err != nil { + return nil, fmt.Errorf("failed to canonicalize agent card: %w", err) + } + + protectedBytes, err := json.Marshal(protectedHeader) + if err != nil { + return nil, fmt.Errorf("failed to marshal protected header: %w", err) + } + protectedB64 := base64.RawURLEncoding.EncodeToString(protectedBytes) + payloadB64 := base64.RawURLEncoding.EncodeToString(canonPayload) + + signingInput := protectedB64 + "." + payloadB64 + + sigBytes, err := method.Sign(signingInput, privateKey) + if err != nil { + return nil, fmt.Errorf("failed to sign agent card: %w", err) + } + + sig := a2a.AgentCardSignature{ + Protected: protectedB64, + Signature: base64.RawURLEncoding.EncodeToString(sigBytes), + } + + cardCopy.Signatures = append(cardCopy.Signatures, sig) + return &cardCopy, nil + } +} + +// createSignatureVerifier creates a function that verifies the signatures on an AgentCard. +func createSignatureVerifier(keyProvider KeyProvider, allowedAlgs []string) func(card *a2a.AgentCard) error { + return func(card *a2a.AgentCard) error { + if len(card.Signatures) == 0 { + return ErrNoSignature + } + + canonPayload, err := canonicalizeAgentCard(card) + if err != nil { + return fmt.Errorf("failed to canonicalize agent card: %w", err) + } + payloadB64 := base64.RawURLEncoding.EncodeToString(canonPayload) + + for _, sig := range card.Signatures { + protectedBytes, err := base64.RawURLEncoding.DecodeString(sig.Protected) + if err != nil { + continue + } + var protectedHeader ProtectedHeader + if err := json.Unmarshal(protectedBytes, &protectedHeader); err != nil { + continue + } + + algAllowed := false + for _, alg := range allowedAlgs { + if protectedHeader.Alg == alg { + algAllowed = true + break + } + } + if !algAllowed { + continue + } + + verificationKey, err := keyProvider(protectedHeader.Kid, protectedHeader.Jku) + if err != nil { + continue + } + + tokenStr := sig.Protected + "." + payloadB64 + "." + sig.Signature + token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (any, error) { + if token.Method.Alg() != protectedHeader.Alg { + return nil, fmt.Errorf("unexpected signing algorithm: %v", token.Header["alg"]) + } + return verificationKey, nil + }) + + if err == nil && token.Valid { + return nil + } + } + + return ErrInvalidSignatures + } +} diff --git a/samples/go/agents/signing_and_verifying/test_client.go b/samples/go/agents/signing_and_verifying/test_client.go new file mode 100644 index 000000000..5129d725e --- /dev/null +++ b/samples/go/agents/signing_and_verifying/test_client.go @@ -0,0 +1,107 @@ +package main + +import ( + "context" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "log" + "net/http" + + "github.com/a2aproject/a2a-go/a2a" + "github.com/a2aproject/a2a-go/a2aclient" + "github.com/a2aproject/a2a-go/a2aclient/agentcard" + "github.com/a2aproject/a2a-go/a2asrv" +) + +func keyProvider(kid, jku string) (any, error) { + if kid == "" || jku == "" { + log.Println("kid or jku missing") + return nil, fmt.Errorf("kid or jku missing") + } + + resp, err := http.Get(jku) + if err != nil { + return nil, fmt.Errorf("failed to fetch jku: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("jku request failed with status: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read jku body: %w", err) + } + + var keys map[string]string + if err := json.Unmarshal(body, &keys); err != nil { + return nil, fmt.Errorf("failed to unmarshal keys JSON: %w", err) + } + + pemStr, ok := keys[kid] + if !ok { + return nil, fmt.Errorf("key id %s not found in jku", kid) + } + + block, _ := pem.Decode([]byte(pemStr)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + + pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse PKIX public key: %w", err) + } + + return pubKey, nil +} + +func runTestClient() { + ctx := context.Background() + signatureVerifier := createSignatureVerifier(keyProvider, []string{"ES256"}) + + baseURL := "http://localhost:9999" + + log.Printf("Attempting to fetch public agent card from: %s%s", baseURL, a2asrv.WellKnownAgentCardPath) + + // Initialize A2ACardResolver + resolver := agentcard.NewResolver(http.DefaultClient) + publicCard, err := resolver.Resolve(ctx, baseURL) + if err != nil { + log.Fatalf("Critical error fetching public agent card: %v", err) + } + + // Verifies the AgentCard using signature_verifier function before returning it + if err := signatureVerifier(publicCard); err != nil { + log.Fatalf("Failed to verify public agent card signature: %v", err) + } + + log.Println("Successfully fetched public agent card:") + cardJSON, _ := json.MarshalIndent(publicCard, "", " ") + log.Println(string(cardJSON)) + log.Println("\nUsing PUBLIC agent card for client initialization (default).") + + // Create Base Client directly via unified factory + client, err := a2aclient.NewFromCard(ctx, publicCard) + if err != nil { + log.Fatalf("Failed to create A2A client: %v", err) + } + + extendedCard, err := client.GetExtendedAgentCard(ctx, &a2a.GetExtendedAgentCardRequest{}) + if err != nil { + log.Fatalf("Failed to get extended agent card: %v", err) + } + + // Verifies the AgentCard using signature_verifier function before returning it + if err := signatureVerifier(extendedCard); err != nil { + log.Fatalf("Failed to verify extended agent card signature: %v", err) + } + + fmt.Println("Fetched extended card:") + extJSON, _ := json.MarshalIndent(extendedCard, "", " ") + fmt.Println(string(extJSON)) +} From 538d005f78eb9622e6f1216a18270a9adae097b8 Mon Sep 17 00:00:00 2001 From: Iwaniukooo11 Date: Tue, 30 Jun 2026 13:26:34 +0000 Subject: [PATCH 2/2] fix(samples/go): resolve Super-Linter issues in signing and verifying sample --- .../signing_and_verifying/Containerfile | 2 +- .../go/agents/signing_and_verifying/README.md | 21 +++++++++---- .../go/agents/signing_and_verifying/main.go | 30 +++++++++++-------- .../agents/signing_and_verifying/signing.go | 6 ++-- .../signing_and_verifying/test_client.go | 27 ++++++++++------- 5 files changed, 55 insertions(+), 31 deletions(-) diff --git a/samples/go/agents/signing_and_verifying/Containerfile b/samples/go/agents/signing_and_verifying/Containerfile index cb1c19319..97a1e1405 100644 --- a/samples/go/agents/signing_and_verifying/Containerfile +++ b/samples/go/agents/signing_and_verifying/Containerfile @@ -9,7 +9,7 @@ COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o agent . -FROM alpine:latest +FROM alpine:3.21 WORKDIR /app diff --git a/samples/go/agents/signing_and_verifying/README.md b/samples/go/agents/signing_and_verifying/README.md index 14d7f704f..d3105549d 100644 --- a/samples/go/agents/signing_and_verifying/README.md +++ b/samples/go/agents/signing_and_verifying/README.md @@ -57,8 +57,19 @@ python3 __main__.py --agent http://localhost:9999 ## Disclaimer -Important: The sample code provided is for demonstration purposes and illustrates the mechanics of the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity. - -All data received from an external agent—including but not limited to its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide an AgentCard containing crafted data in its fields (e.g., description, name, skills.description). If this data is used without sanitization to construct prompts for a Large Language Model (LLM), it could expose your application to prompt injection attacks. Failure to properly validate and sanitize this data before use can introduce security vulnerabilities into your application. - -Developers are responsible for implementing appropriate security measures, such as input validation and secure handling of credentials to protect their systems and users. +Important: The sample code provided is for demonstration purposes +and illustrates the mechanics of the Agent-to-Agent (A2A) protocol. +When building production applications, it is critical to treat any agent +operating outside of your direct control as a potentially untrusted entity. + +All data received from an external agent—including but not limited to its AgentCard, +messages, artifacts, and task statuses—should be handled as untrusted input. +For example, a malicious agent could provide an AgentCard containing crafted data +in its fields (e.g., description, name, skills.description). If this data is used +without sanitization to construct prompts for a Large Language Model (LLM), +it could expose your application to prompt injection attacks. Failure to properly +validate and sanitize this data before use can introduce security vulnerabilities +into your application. + +Developers are responsible for implementing appropriate security measures, +such as input validation and secure handling of credentials to protect their systems and users. diff --git a/samples/go/agents/signing_and_verifying/main.go b/samples/go/agents/signing_and_verifying/main.go index 0fc9e1ec8..52f13f4a2 100644 --- a/samples/go/agents/signing_and_verifying/main.go +++ b/samples/go/agents/signing_and_verifying/main.go @@ -17,6 +17,12 @@ import ( "github.com/a2aproject/a2a-go/a2asrv" ) +const ( + modeText = "text" + serverURL = "http://localhost:9999" + es256Alg = "ES256" +) + func main() { runClientFlag := flag.Bool("client", false, "Run the test client instead of starting server only") flag.Parse() @@ -44,8 +50,8 @@ func main() { if err != nil { log.Fatalf("Failed to marshal keys JSON: %v", err) } - if err := os.WriteFile("public_keys.json", keysJSON, 0644); err != nil { - log.Fatalf("Failed to save public_keys.json: %v", err) + if writeErr := os.WriteFile("public_keys.json", keysJSON, 0600); writeErr != nil { + log.Fatalf("Failed to save public_keys.json: %v", writeErr) } skill := a2a.AgentSkill{ @@ -67,16 +73,16 @@ func main() { publicAgentCard := &a2a.AgentCard{ Name: "Signed Agent", Description: "An Agent that is signed", - IconURL: "http://localhost:9999/", + IconURL: serverURL + "/", Version: "1.0.0", - DefaultInputModes: []string{"text"}, - DefaultOutputModes: []string{"text"}, + DefaultInputModes: []string{modeText}, + DefaultOutputModes: []string{modeText}, Capabilities: a2a.AgentCapabilities{Streaming: true, ExtendedAgentCard: true}, SupportedInterfaces: []*a2a.AgentInterface{ { ProtocolBinding: a2a.TransportProtocolJSONRPC, ProtocolVersion: a2a.Version, - URL: "http://localhost:9999", + URL: serverURL, }, }, Skills: []a2a.AgentSkill{skill}, @@ -85,16 +91,16 @@ func main() { extendedAgentCard := &a2a.AgentCard{ Name: "Signed Agent - Extended Edition", Description: "The full-featured signed agent for authenticated users.", - IconURL: "http://localhost:9999/", + IconURL: serverURL + "/", Version: "1.0.1", - DefaultInputModes: []string{"text"}, - DefaultOutputModes: []string{"text"}, + DefaultInputModes: []string{modeText}, + DefaultOutputModes: []string{modeText}, Capabilities: a2a.AgentCapabilities{Streaming: true, ExtendedAgentCard: true}, SupportedInterfaces: []*a2a.AgentInterface{ { ProtocolBinding: a2a.TransportProtocolJSONRPC, ProtocolVersion: a2a.Version, - URL: "http://localhost:9999", + URL: serverURL, }, }, Skills: []a2a.AgentSkill{ @@ -108,8 +114,8 @@ func main() { privateKey, ProtectedHeader{ Kid: kid, - Alg: "ES256", - Jku: "http://localhost:9999/public_keys.json", + Alg: es256Alg, + Jku: serverURL + "/public_keys.json", }, ) diff --git a/samples/go/agents/signing_and_verifying/signing.go b/samples/go/agents/signing_and_verifying/signing.go index 95773f2e3..43083edba 100644 --- a/samples/go/agents/signing_and_verifying/signing.go +++ b/samples/go/agents/signing_and_verifying/signing.go @@ -14,7 +14,7 @@ import ( // Sentinel errors for signature verification matching Python SDK exception types. var ( ErrNoSignature = errors.New("AgentCard has no signatures to verify") - ErrInvalidSignatures = errors.New("No valid signature found") + ErrInvalidSignatures = errors.New("no valid signature found") ) // ProtectedHeader defines the protected header parameters for JWS (RFC 7515). @@ -84,7 +84,7 @@ func canonicalizeAgentCard(card *a2a.AgentCard) ([]byte, error) { // createAgentCardSigner creates a function that signs an AgentCard and appends the signature. func createAgentCardSigner(privateKey *ecdsa.PrivateKey, protectedHeader ProtectedHeader) func(card *a2a.AgentCard) (*a2a.AgentCard, error) { if protectedHeader.Alg == "" { - protectedHeader.Alg = "ES256" + protectedHeader.Alg = es256Alg } method := jwt.GetSigningMethod(protectedHeader.Alg) @@ -145,7 +145,7 @@ func createSignatureVerifier(keyProvider KeyProvider, allowedAlgs []string) func continue } var protectedHeader ProtectedHeader - if err := json.Unmarshal(protectedBytes, &protectedHeader); err != nil { + if unmarshalErr := json.Unmarshal(protectedBytes, &protectedHeader); unmarshalErr != nil { continue } diff --git a/samples/go/agents/signing_and_verifying/test_client.go b/samples/go/agents/signing_and_verifying/test_client.go index 5129d725e..1c7e875e7 100644 --- a/samples/go/agents/signing_and_verifying/test_client.go +++ b/samples/go/agents/signing_and_verifying/test_client.go @@ -22,6 +22,7 @@ func keyProvider(kid, jku string) (any, error) { return nil, fmt.Errorf("kid or jku missing") } + //nolint:gosec // JKU URL is dynamic by design per RFC 7515 resp, err := http.Get(jku) if err != nil { return nil, fmt.Errorf("failed to fetch jku: %w", err) @@ -38,8 +39,8 @@ func keyProvider(kid, jku string) (any, error) { } var keys map[string]string - if err := json.Unmarshal(body, &keys); err != nil { - return nil, fmt.Errorf("failed to unmarshal keys JSON: %w", err) + if unmarshalErr := json.Unmarshal(body, &keys); unmarshalErr != nil { + return nil, fmt.Errorf("failed to unmarshal keys JSON: %w", unmarshalErr) } pemStr, ok := keys[kid] @@ -62,9 +63,9 @@ func keyProvider(kid, jku string) (any, error) { func runTestClient() { ctx := context.Background() - signatureVerifier := createSignatureVerifier(keyProvider, []string{"ES256"}) + signatureVerifier := createSignatureVerifier(keyProvider, []string{es256Alg}) - baseURL := "http://localhost:9999" + baseURL := serverURL log.Printf("Attempting to fetch public agent card from: %s%s", baseURL, a2asrv.WellKnownAgentCardPath) @@ -76,12 +77,15 @@ func runTestClient() { } // Verifies the AgentCard using signature_verifier function before returning it - if err := signatureVerifier(publicCard); err != nil { - log.Fatalf("Failed to verify public agent card signature: %v", err) + if verifyErr := signatureVerifier(publicCard); verifyErr != nil { + log.Fatalf("Failed to verify public agent card signature: %v", verifyErr) } log.Println("Successfully fetched public agent card:") - cardJSON, _ := json.MarshalIndent(publicCard, "", " ") + cardJSON, err := json.MarshalIndent(publicCard, "", " ") + if err != nil { + log.Fatalf("Failed to marshal public agent card: %v", err) + } log.Println(string(cardJSON)) log.Println("\nUsing PUBLIC agent card for client initialization (default).") @@ -97,11 +101,14 @@ func runTestClient() { } // Verifies the AgentCard using signature_verifier function before returning it - if err := signatureVerifier(extendedCard); err != nil { - log.Fatalf("Failed to verify extended agent card signature: %v", err) + if verifyErr := signatureVerifier(extendedCard); verifyErr != nil { + log.Fatalf("Failed to verify extended agent card signature: %v", verifyErr) } fmt.Println("Fetched extended card:") - extJSON, _ := json.MarshalIndent(extendedCard, "", " ") + extJSON, err := json.MarshalIndent(extendedCard, "", " ") + if err != nil { + log.Fatalf("Failed to marshal extended agent card: %v", err) + } fmt.Println(string(extJSON)) }