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
3 changes: 3 additions & 0 deletions samples/go/agents/signing_and_verifying/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# File generated for storing the public key used in verification of AgentCard signature
public_keys.json
signing_and_verifying
20 changes: 20 additions & 0 deletions samples/go/agents/signing_and_verifying/Containerfile
Original file line number Diff line number Diff line change
@@ -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:3.21

WORKDIR /app

COPY --from=builder /app/agent .

EXPOSE 9999

CMD ["./agent"]
75 changes: 75 additions & 0 deletions samples/go/agents/signing_and_verifying/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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.
37 changes: 37 additions & 0 deletions samples/go/agents/signing_and_verifying/agent_executor.go
Original file line number Diff line number Diff line change
@@ -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"))
}
}
15 changes: 15 additions & 0 deletions samples/go/agents/signing_and_verifying/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
20 changes: 20 additions & 0 deletions samples/go/agents/signing_and_verifying/go.sum
Original file line number Diff line number Diff line change
@@ -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=
170 changes: 170 additions & 0 deletions samples/go/agents/signing_and_verifying/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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"
)

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()

// 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 writeErr := os.WriteFile("public_keys.json", keysJSON, 0600); writeErr != nil {
log.Fatalf("Failed to save public_keys.json: %v", writeErr)
}

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: serverURL + "/",
Version: "1.0.0",
DefaultInputModes: []string{modeText},
DefaultOutputModes: []string{modeText},
Capabilities: a2a.AgentCapabilities{Streaming: true, ExtendedAgentCard: true},
SupportedInterfaces: []*a2a.AgentInterface{
{
ProtocolBinding: a2a.TransportProtocolJSONRPC,
ProtocolVersion: a2a.Version,
URL: serverURL,
},
},
Skills: []a2a.AgentSkill{skill},
}

extendedAgentCard := &a2a.AgentCard{
Name: "Signed Agent - Extended Edition",
Description: "The full-featured signed agent for authenticated users.",
IconURL: serverURL + "/",
Version: "1.0.1",
DefaultInputModes: []string{modeText},
DefaultOutputModes: []string{modeText},
Capabilities: a2a.AgentCapabilities{Streaming: true, ExtendedAgentCard: true},
SupportedInterfaces: []*a2a.AgentInterface{
{
ProtocolBinding: a2a.TransportProtocolJSONRPC,
ProtocolVersion: a2a.Version,
URL: serverURL,
},
},
Skills: []a2a.AgentSkill{
skill,
extendedSkill,
},
}

// Create signer function which will be used for AgentCard signing
signer := createAgentCardSigner(
privateKey,
ProtectedHeader{
Kid: kid,
Alg: es256Alg,
Jku: serverURL + "/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)
}
}
Loading