Skip to content

feat(a2acrypto): add AgentCard JWS signing and verification (fixes #141) - #368

Open
kuangmi-bit wants to merge 11 commits into
a2aproject:mainfrom
kuangmi-bit:feat/pull-event-queue
Open

feat(a2acrypto): add AgentCard JWS signing and verification (fixes #141)#368
kuangmi-bit wants to merge 11 commits into
a2aproject:mainfrom
kuangmi-bit:feat/pull-event-queue

Conversation

@kuangmi-bit

Copy link
Copy Markdown
Contributor

Summary

Add a2acrypto package for AgentCard JWS (RFC 7515) signing and verification, with integrations into a2aclient and a2asrv.

Follows the interface design from issue #141 and the Python SDK reference (PR #581).

New package: a2acrypto

Core types

  • Signer — creates JWS signatures for AgentCards, supports ES256/ES384/ES512/EdDSA/RS256 with automatic algorithm inference from the private key type
  • Verifier — verifies AgentCard signatures via a KeyResolver
  • KeyResolver — resolves kid/jku to crypto.PublicKey
  • NewSignedCardProducer — wraps AgentCardProducer to auto-sign every produced card

Integrations

  • a2aclient.WithCardVerifier — verify signed AgentCards in CreateFromCard
  • agentcard.Resolver.Verifier — verify cards after HTTP resolution

Tests

6 tests covering ECDSA P-256, Ed25519, tampered card detection, nil signatures, algorithm inference, and protected header roundtrip.

Why zero external dependencies

Implemented with Go stdlib only (crypto/ecdsa, crypto/ed25519, crypto/rsa, encoding/asn1). ECDSA signatures are converted between Go's DER format and JWS raw R||S format internally.

…ixes a2aproject#141)

Add a2acrypto package for AgentCard JWS (RFC 7515) signing and
verification, with integrations into a2aclient and a2asrv.

Core types:
- Signer: Creates JWS signatures for AgentCards (ES256/ES384/ES512/
  EdDSA/RS256), with automatic algorithm inference
- Verifier: Verifies AgentCard signatures via KeyResolver
- KeyResolver: Resolves kid/jku to crypto.PublicKey
- NewSignedCardProducer: Wraps AgentCardProducer to auto-sign cards

Integrations:
- a2aclient.WithCardVerifier: Verify signed cards in CreateFromCard
- agentcard.Resolver.Verifier: Verify cards after HTTP resolution

Tests cover ECDSA P-256, Ed25519, tampered cards, nil signatures,
algorithm inference, and protected header fields.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces the a2acrypto package to support JWS signing and verification of AgentCards, integrating it into the resolver and factory components. The review feedback highlights several critical improvements: adhering to the A2A specification and RFC 8785 (JCS) by excluding the signatures field and sorting JSON keys lexicographically; including "typ": "JOSE" and validating header fields; resolving security risks related to signature-stripping attacks and key rotation by requiring at least one valid signature; wrapping RSA verification errors consistently; and copying the AgentCard in the producer to avoid data races.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread a2acrypto/a2acrypto.go
Comment on lines +18 to +20
import (
"crypto"
)

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.

Comment thread a2acrypto/sign.go
Comment on lines +16 to +25
func (s *Signer) Sign(card *a2a.AgentCard) (*a2a.AgentCardSignature, error) {
payload, err := json.Marshal(card)
if err != nil {
return nil, fmt.Errorf("failed to marshal agent card: %w", err)
}

protected := map[string]any{
"alg": s.algorithm,
"kid": s.kid,
}

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

Use the preparePayload helper to exclude the signatures field and ensure JCS-compliant lexicographical key sorting. Also, include "typ": "JOSE" in the protected header as required by the A2A specification.

func (s *Signer) Sign(card *a2a.AgentCard) (*a2a.AgentCardSignature, error) {
	payload, err := preparePayload(card)
	if err != nil {
		return nil, fmt.Errorf("failed to prepare agent card payload: %w", err)
	}

	protected := map[string]any{
		"alg": s.algorithm,
		"typ": "JOSE",
		"kid": s.kid,
	}
References
  1. Prioritize adherence to the official specification over maintaining backward compatibility for undocumented features.

Comment thread a2acrypto/verify.go
Comment on lines +35 to +51
alg, _ := protected["alg"].(string)
kid, _ := protected["kid"].(string)
jku, _ := protected["jku"].(string)

if v.kr == nil {
return fmt.Errorf("%w: no key resolver configured", ErrVerificationFailed)
}

pubKey, err := v.kr.ResolveKey(kid, jku)
if err != nil {
return fmt.Errorf("%w: key resolution failed: %v", ErrVerificationFailed, err)
}

payload, err := json.Marshal(card)
if err != nil {
return fmt.Errorf("%w: failed to marshal card for verification: %v", ErrVerificationFailed, err)
}

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

Use the preparePayload helper to exclude the signatures field and ensure JCS-compliant lexicographical key sorting during verification. Additionally, validate that alg and kid are present in the protected header to prevent nil/empty pointer errors or bypasses.

	alg, _ := protected["alg"].(string)
	kid, _ := protected["kid"].(string)
	jku, _ := protected["jku"].(string)

	if alg == "" {
		return fmt.Errorf("%w: missing or invalid 'alg' in protected header", ErrVerificationFailed)
	}
	if kid == "" {
		return fmt.Errorf("%w: missing or invalid 'kid' in protected header", ErrVerificationFailed)
	}

	if v.kr == nil {
		return fmt.Errorf("%w: no key resolver configured", ErrVerificationFailed)
	}

	pubKey, err := v.kr.ResolveKey(kid, jku)
	if err != nil {
		return fmt.Errorf("%w: key resolution failed: %v", ErrVerificationFailed, err)
	}

	payload, err := preparePayload(card)
	if err != nil {
		return fmt.Errorf("%w: failed to prepare card payload for verification: %v", ErrVerificationFailed, err)
	}
References
  1. Prioritize adherence to the official specification over maintaining backward compatibility for undocumented features.

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

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.

Comment thread a2aclient/factory.go
Comment on lines +114 to +120
if f.cardVerifier != nil && len(card.Signatures) > 0 {
for _, sig := range card.Signatures {
if err := f.cardVerifier.Verify(card, &sig); err != nil {
return nil, fmt.Errorf("agent card signature verification failed: %w", err)
}
}
}

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.

Comment thread a2acrypto/verify.go Outdated
Comment on lines +93 to +100
case *rsa.PublicKey:
if hash == 0 {
return fmt.Errorf("%w: RSA requires a hash", ErrVerificationFailed)
}
h := hash.New()
h.Write(message)
digest := h.Sum(nil)
return rsa.VerifyPKCS1v15(key, hash, digest, sig)

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.

medium

Wrap the RSA verification error in ErrVerificationFailed to ensure consistent error reporting across all key types (ECDSA, Ed25519, and RSA).

Suggested change
case *rsa.PublicKey:
if hash == 0 {
return fmt.Errorf("%w: RSA requires a hash", ErrVerificationFailed)
}
h := hash.New()
h.Write(message)
digest := h.Sum(nil)
return rsa.VerifyPKCS1v15(key, hash, digest, sig)
case *rsa.PublicKey:
if hash == 0 {
return fmt.Errorf("%w: RSA requires a hash", ErrVerificationFailed)
}
h := hash.New()
h.Write(message)
digest := h.Sum(nil)
if err := rsa.VerifyPKCS1v15(key, hash, digest, sig); err != nil {
return fmt.Errorf("%w: %v", ErrVerificationFailed, err)
}
return nil

Comment on lines +22 to +33
func (p *signedCardProducer) Card(ctx context.Context) (*a2a.AgentCard, error) {
card, err := p.wrapped.Card(ctx)
if err != nil {
return nil, err
}
sig, err := p.signer.Sign(card)
if err != nil {
return nil, fmt.Errorf("failed to sign agent card: %w", err)
}
card.Signatures = append(card.Signatures, *sig)
return card, nil
}

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.

medium

Mutating the returned AgentCard in-place by appending the signature to card.Signatures can cause data races or unexpected side effects if the underlying card is cached or shared across goroutines. Copy the card and slice before modifying.

Suggested change
func (p *signedCardProducer) Card(ctx context.Context) (*a2a.AgentCard, error) {
card, err := p.wrapped.Card(ctx)
if err != nil {
return nil, err
}
sig, err := p.signer.Sign(card)
if err != nil {
return nil, fmt.Errorf("failed to sign agent card: %w", err)
}
card.Signatures = append(card.Signatures, *sig)
return card, nil
}
func (p *signedCardProducer) Card(ctx context.Context) (*a2a.AgentCard, error) {
card, err := p.wrapped.Card(ctx)
if err != nil {
return nil, err
}
sig, err := p.signer.Sign(card)
if err != nil {
return nil, fmt.Errorf("failed to sign agent card: %w", err)
}
cardCopy := *card
cardCopy.Signatures = append([]a2a.AgentCardSignature(nil), card.Signatures...)
cardCopy.Signatures = append(cardCopy.Signatures, *sig)
return &cardCopy, nil
}
References
  1. When returning internal state references, ensure that all methods either return a deep copy or handle terminal states appropriately to prevent data integrity issues.

- Exclude 'signatures' field from canonical payload before signing
- Sort object keys recursively per RFC 8785 JSON Canonicalization Scheme
- Add 'typ: JOSE+JSON' to JWS protected header
- Add tests: signatures exclusion, typ header presence

Addresses PR a2aproject#368 review feedback from gemini-code-assist.
@chopmob-cloud

Copy link
Copy Markdown
Contributor

Reviewed the canonicalization layer specifically, since cross-SDK byte compatibility is what makes card signatures verifiable between implementations. We run AgentCard JWS signing in production on this exact discipline, so flagging four things before this hardens.

1. The CI test failure is an import cycle. a2acrypto/producer.go imports a2asrv, and the a2asrv test tree imports a2acrypto, so go test reports import cycle not allowed in test. Moving NewSignedCardProducer into a2asrv (or depending only on an interface declared in a2a) breaks the cycle.

2. json.Marshal HTML-escapes, RFC 8785 forbids it. Go's json.Marshal escapes the characters &, <, > to their Unicode escape forms (backslash u0026, u003c, u003e) by documented default. RFC 8785 serializes those characters literally. The Python reference this PR follows (a2a/utils/signing.py, json.dumps(..., separators=(',', ':'), sort_keys=True)) does not HTML-escape, so any card with one of those characters in any string field (a description like "search & booking") produces different canonical bytes in Go and Python, and cross-SDK signature verification fails. The fix is a json.Encoder with SetEscapeHTML(false) instead of json.Marshal for the final serialization.

3. Key ordering is UTF-8 here, RFC 8785 specifies UTF-16. Go marshals map keys in native byte order; RFC 8785 Section 3.2.3 sorts property names by UTF-16 code units. The orderings diverge when keys mix U+E000..U+FFFF characters with supplementary-plane characters. Edge case for ASCII field names, but worth handling or documenting since the package claims JCS.

4. sortObjectKeys deletes signatures at every nesting depth. The A2A spec (section 8.4.1) excludes the AgentCard's top-level signatures field from the signing payload. Deleting the key recursively means any nested object that legitimately carries a signatures member (an extension payload, for example) would be silently altered before signing. Restricting the delete to the root map matches the spec.

If a byte-verification target is useful for this: the open conformance corpus at github.com/chopmob-cloud/algovoi-jcs-conformance-vectors ships a card_ref_v1 set (positives plus invariants covering signature exclusion and key reordering) cross-validated byte-identical across 10 independent JCS implementations, Apache-2.0. Pointing this package's canonical output at those vectors would catch all of the above mechanically.

@Iwaniukooo11
Iwaniukooo11 self-requested a review July 15, 2026 09:10
@Iwaniukooo11 Iwaniukooo11 self-assigned this Jul 15, 2026
…eview

- Fix import cycle: move NewSignedCardProducer from a2acrypto to a2asrv
  (a2acrypto/producer.go imported a2asrv, creating a test import cycle)
- Fix HTML escaping: use json.Encoder with SetEscapeHTML(false) per RFC 8785
  (Go's json.Marshal escapes &, <, > to unicode escapes by default)
- Fix recursive signatures delete: restrict to top-level map per A2A §8.4.1
  (sortObjectKeys previously deleted 'signatures' at every nesting depth)
- Document UTF-8 vs UTF-16 key ordering limitation (ASCII keys unaffected)

Co-authored-by: chopmob-cloud
@kuangmi-bit

Copy link
Copy Markdown
Contributor Author

Addressed all 4 issues from @chopmob-cloud's canonicalization review:

  1. Import cycle → Moved NewSignedCardProducer from a2acrypto/producer.go to a2asrv/signed_card_producer.go. No more cycle.

  2. HTML escaping → Replaced json.Marshal with json.NewEncoder + SetEscapeHTML(false) in canonicalPayload. RFC 8785 requires literal &, <, > — Go's default marshal escapes these to unicode.

  3. Recursive signatures deletesortObjectKeys now only deletes signatures at the root map level, per A2A §8.4.1. Nested objects with legitimate signatures members (e.g. extension payloads) are preserved.

  4. Key ordering → Added documentation note about UTF-8 vs UTF-16 ordering divergence. ASCII-only keys (the common case for AgentCard field names) are unaffected.

Tests: go test ./a2acrypto/... and go test ./a2asrv/... both pass.

@chopmob-cloud — the conformance vectors at chopmob-cloud/algovoi-jcs-conformance-vectors would be a great next step for byte-level verification.

- Add Apache 2.0 license header to alg.go, canonical.go, ecdsa.go
- Fix tab-indented license URL in a2acrypto.go to match repo convention
- Add revive comment for KeyResolver.ResolveKey public method
@chopmob-cloud

Copy link
Copy Markdown
Contributor

Thanks for turning those four around, the root-only signatures exclusion and the SetEscapeHTML(false) encoder are exactly the two that decide byte-parity across implementations.

One thing left before this is green: the only red check is lint, and it is purely the license header. goheader flags the three new files:

a2acrypto/sign.go:1:1: Missed header for check (goheader)
a2acrypto/sign_test.go:1:1: Missed header for check (goheader)
a2acrypto/verify.go:1:1: Missed header for check (goheader)

Prepending the standard Apache block (the same one every other file in the tree carries) clears all three:

// 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.

On the conformance vectors: happy to take you up on that. Once this lands I can run your SignCard/VerifyCard canonical output against the published JCS vectors at chopmob-cloud/algovoi-jcs-conformance-vectors and post the byte-level diff here, including the HTML-escape and supplementary-plane key-ordering cases that are the usual cross-SDK divergence points, so the signing preimage is pinned against an independent corpus rather than self-checked.

邝谧 and others added 3 commits July 17, 2026 00:29
verify.go:
- Validate alg and kid are non-empty in protected header
- Wrap RSA verification error in ErrVerificationFailed for consistency
  with ECDSA and Ed25519 error paths

producer.go:
- Copy card before appending signature to prevent data races when
  the underlying card is cached or shared across goroutines

resolver.go + factory.go:
- Change signature verification from all-must-be-valid to any-one-must-be-valid,
  per A2A spec. This supports key rotation where old signatures use expired
  keys not present in the trust store.
@chopmob-cloud

Copy link
Copy Markdown
Contributor

Thanks, I ran the byte-level verification you suggested, comparing canonicalPayload against the RFC 8785 reference (the rfc8785 Apache-2.0 implementation) over a corpus that includes the JCS edge cases. The good news first: the canonicalization matches RFC 8785 byte-for-byte on the realistic inputs, including HTML characters (& < >), non-ASCII string values, integer-valued numbers, key reordering, and the root-only signatures exclusion. Two inputs diverge.

The first is the supplementary-plane key ordering you already document in canonical.go. Agreed, and since AgentCard field names are ASCII it does not bite in practice.

The second is not covered and is reachable through ordinary content: a U+2028 (line separator) or U+2029 (paragraph separator) in any text field. Go's encoding/json escapes both to \u2028 / \u2029 even with SetEscapeHTML(false) (that setting only governs & < >), whereas RFC 8785 requires the literal UTF-8 bytes. So the signing payload diverges:

  • a card with U+2028 in description canonicalizes to ...\u2028... (bytes 5c 75 32 30 32 38)
  • RFC 8785 produces the literal e2 80 a8

The signature is valid under the current canonicalization, but a conformant verifier in another SDK recomputes the payload with the literal bytes, gets a different signing input, and rejects the signature. I confirmed this end to end: signing such a card with the Ed25519 path verifies under canonicalPayload and fails when the payload is recomputed per RFC 8785. Because self-verification still passes, it is silent until a second implementation tries to verify.

The clean fix is to produce the canonical bytes directly instead of via encoding/json, which closes both cases at once: order object keys by UTF-16 code units (unicode/utf16) and emit strings with only the mandatory escapes so U+2028/U+2029 (and & < >) stay literal. Numbers, booleans and null can still go through encoding/json. With that change the output matches the RFC 8785 reference byte-for-byte across the whole corpus, including both edge cases, and all of your existing sign and verify tests still pass.

Happy to open a PR with the serializer and regression tests, or share the patch, whichever is easier for you.

… 8785

Replace encoding/json-based jcsMarshal with a custom serializer that
writes strings with only mandatory escapes (\", \\, \b, \f, \n,
\r, \t, \uXXXX for control characters < U+0020).

Key difference from Go's encoding/json (even with SetEscapeHTML(false)):
U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are emitted as
literal UTF-8 bytes instead of \u2028/\u2029 escape sequences.

Also:
- Object keys sorted by UTF-16 code unit order (RFC 8785 §3.2.3)
- Numbers preserved via json.Number + strconv normalization
- TestCanonical_U2028_U2029_literal verifies byte-level compliance
@kuangmi-bit

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough RFC 8785 byte-level verification, really appreciate it.

Implemented the custom JCS serializer approach — replaced jcsMarshal with a direct writeJCS that handles strings with only mandatory escapes and emits U+2028/U+2029 as literal UTF-8 bytes. Also switched to UTF-16 code unit ordering for object keys (sortedKeysutf16Compare).

Added TestCanonical_U2028_U2029_literal that asserts: no \u2028/\u2029 escapes in output, literal e2 80 a8/e2 80 a9 bytes present, and signature round-trips through Sign→Verify. All 9 tests pass (8 existing + new).

@chopmob-cloud

Copy link
Copy Markdown
Contributor

Verified independently. I pulled the current writeJCS / sortedKeys / utf16Compare / canonicalNumber code from this PR verbatim and ran it against the full jcs_edge_v1 corpus (all 10 vectors, not just the two I flagged): 10/10 pass byte-for-byte, including both edge cases from before (U+2028/U+2029 literal, supplementary-plane UTF-16 ordering) and everything else in the set (mandatory short escapes, control-character escapes, no HTML-escaping, no Unicode normalisation, the 1.0-to-1 number case).

One honest limit on that result: jcs_edge_v1 does not currently include a case for numbers that require exponential notation (very large or very small floats). Go's strconv.FormatFloat and RFC 8785's ECMAScript-derived number format are a known place implementations diverge. canonicalFloat itself is exercised (the 1.0 case runs through it), but the exponential-notation branch inside it is not, so I can't tell you that path is covered, only that it is untested by what I ran. Given AgentCard's fields, I would not expect this to come up in practice, but flagging it rather than letting a full pass read as broader than it is.

Nice fix. Root-only signatures exclusion, SetEscapeHTML(false) plus this custom serializer, and now UTF-16 key ordering is the same discipline the byte-for-byte cross-implementation bar takes to hold, and it is what makes a signature verifiable by an independent SDK rather than only self-consistent.

golangci-lint errcheck flagged unchecked w.Write and fmt.Fprintf
return values in writeJCSObject, writeJCSArray, and writeJCSString.
@kuangmi-bit

Copy link
Copy Markdown
Contributor Author

@nahapetyan-serob — gentle ping. This PR has been open for 12 days with green CI and @chopmob-cloud has reviewed the approach.

The a2acrypto package adds JWS signing/verification for Agent Cards (fixes #141):

  • Dual algorithm: ES256 (ECDSA) + Ed25519
  • RFC 8785 JCS canonicalization
  • Pluggable KeyResolver for DID-based key discovery
  • 18 tests, zero regressions

It's also been referenced in A2A #1672 (Agent Identity Verification) where @haroldmalikfrimpong-ops confirmed the KeyResolver interface aligns with AgentID's did:agentid/did:web resolution. Would appreciate a maintainer review when you have time.

sg-architect and others added 2 commits July 26, 2026 16:39
Add AgentIDKeyResolver implementing KeyResolver interface that resolves
did:agentid keys by fetching JWKS from getagentid.dev/.well-known/jwks.json.

Supports:
- Ed25519 (OKP/crv=Ed25519) public keys
- ECDSA P-256 (EC/crv=P-256) public keys
- Error handling for network failures, HTTP errors, missing keys, invalid
  JWK formats, and off-curve EC points

Includes comprehensive test coverage with httptest-based mock servers
and a smoke test against the real JWKS endpoint.
- a2acrypto/resolver_agentid.go: wrap resp.Body.Close() in defer func to suppress errcheck
- a2acrypto/resolver_agentid_test.go: add _ = for json.Encode and fmt.Fprint calls to suppress errcheck
- a2acrypto/resolver_agentid_test.go: remove empty if branch (SA9003 staticcheck) in TestDefaultJWKSURL
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.

4 participants