Skip to content

Commit 806c385

Browse files
authored
Merge pull request #41 from ThirdKeyAI/feature/v1.4-alpha.4-trust-bundle-distribution
v1.4-alpha.4: A2A trust-bundle distribution (all four languages)
2 parents 5d5eb47 + 283c5f2 commit 806c385

36 files changed

Lines changed: 2603 additions & 34 deletions

CHANGELOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,59 @@ All notable changes to the SchemaPin project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.4.0-alpha.4] - 2026-06-21
9+
10+
### Added
11+
12+
#### Trust Bundle Distribution for A2A Networks — All Four Languages
13+
14+
Signed trust bundles so they can be exchanged between agents over A2A without
15+
per-bundle out-of-band trust establishment. Roadmap item 6. All additions are
16+
optional fields — existing unsigned bundles are unaffected.
17+
18+
- **Wire format**: trust bundles gain four optional top-level fields —
19+
`bundle_authority` (`{ kid, public_key_pem }`), `signed_at`, `expires_at`,
20+
and `signature` (base64 DER ECDSA P-256). Signing stamps
21+
`schemapin_bundle_version` to `"1.4"`.
22+
- **Signing input**: the signature covers the `schemapin-v1` canonicalization
23+
(recursive sorted keys, compact, UTF-8) of the whole bundle with the
24+
`signature` field set to `""`. The four SDKs produce byte-identical signing
25+
input, so a bundle signed by any SDK verifies in every other (proven by the
26+
shared `tests/cross-language/signed_bundle.json` fixture).
27+
- **`sign_trust_bundle(bundle, private_key_pem, kid, signed_at, expires_at?)`**:
28+
derives the authority public key from the private key, stamps metadata, and
29+
writes the signature. (Camel/Pascal-cased per language.)
30+
- **`verify_trust_bundle(bundle, authority_pin_store)`**: requires a signed
31+
bundle (else `BUNDLE_UNSIGNED`), rejects past-`expires_at` bundles
32+
(`BUNDLE_EXPIRED`), TOFU-pins the authority key by `kid` (mismatch →
33+
`KEY_PIN_MISMATCH`), then verifies the signature (`SIGNATURE_INVALID`).
34+
- **`merge_trust_bundles(bundles)`**: deduplicates discovery and revocation
35+
documents by domain, newest source (`signed_at`, else `created_at`) winning.
36+
Returns an unsigned merged bundle to be re-signed before redistribution.
37+
- **`schemapin/trustBundle` JSON-RPC envelope helpers**
38+
(`build_trust_bundle_request` / `build_trust_bundle_response` /
39+
`parse_trust_bundle_response`) for A2A bundle exchange. The libraries provide
40+
the message envelope; transport and the receiving pin-store update are the
41+
host application's (e.g. Symbiont's) responsibility.
42+
- **New error codes**: `BUNDLE_UNSIGNED`, `BUNDLE_EXPIRED`.
43+
44+
### Changed
45+
46+
- All four implementations bumped to `1.4.0-alpha.4` (`1.4.0a4` for Python).
47+
- **Deviation from the scope doc**: `bundle_authority` carries the authority
48+
key as `public_key_pem` (consistent with discovery documents) rather than a
49+
JWK. PEM is a plain string, which keeps the cross-language canonical signing
50+
input trivially identical; JWK would add a key-serialization surface for
51+
cross-SDK drift.
52+
53+
### Notes
54+
55+
- This is item 6 of the v1.4 roadmap. Items 7 (scan-aware signatures) and 8
56+
(cross-agent schema cache) were **descoped to v1.5** — they are additive and
57+
independent, so deferring them does not block the v1.4.0 GA.
58+
- Symbiont follow-up: receive `schemapin/trustBundle` over A2A and update the
59+
local pin store (filed Symbiont-side).
60+
861
## [1.4.0-alpha.3] - 2026-05-16
962

1063
### Added

ROADMAP.md

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SchemaPin Roadmap
22

3-
![Version](https://img.shields.io/badge/current-v1.3.0-brightgreen)
4-
![Next](https://img.shields.io/badge/next-v1.4.0_(planning)-blue)
3+
![Version](https://img.shields.io/badge/current-v1.4.0--alpha.4-blue)
4+
![Next](https://img.shields.io/badge/next-v1.4.0_GA-blue)
55
![License](https://img.shields.io/badge/license-MIT-green)
66

77
**Cryptographic schema integrity verification for AI tool ecosystems — the trust anchor of the ThirdKey trust stack.**
@@ -18,8 +18,10 @@
1818
| **1.3.0** | 2026-02 | AgentSkills security — skill folder signing | **Shipped** |
1919
| **1.4.0-alpha.1** | 2026-04-30 | Signature expiration + DNS TXT cross-verification (all 4 langs) | **Shipped** |
2020
| **1.4.0-alpha.2** | 2026-05-01 | Schema version binding (`schema_version` + `previous_hash` lineage chain, all 4 langs) | **Shipped** |
21-
| **1.4.0** | Q2-Q3 2026 | Signature lifecycle, version binding, A2A trust | In progress |
22-
| **1.5.0** | Q4 2026 | Multi-key endorsement, permissions, advanced revocation | Planning |
21+
| **1.4.0-alpha.3** | 2026-05-16 | Canonicalization algorithm id + A2A verification context (all 4 langs) | **Shipped** |
22+
| **1.4.0-alpha.4** | 2026-06-21 | A2A trust-bundle distribution — bundle signing, merge, TOFU, JSON-RPC (all 4 langs) | **Shipped** |
23+
| **1.4.0** | Q3 2026 | Signature lifecycle, version binding, A2A trust — GA (items 7–8 moved to 1.5) | In progress |
24+
| **1.5.0** | Q4 2026 | Scan-aware signatures, cross-agent schema cache, multi-key endorsement, permissions, advanced revocation | Planning |
2325

2426
---
2527

@@ -145,7 +147,7 @@ AgentPin already uses `_agentpin.{domain}` TXT records, but SchemaPin doesn't le
145147
_schemapin.example.com. IN TXT "v=schemapin1; kid=acme-2026-01; fp=sha256:a1b2c3d4..."
146148
```
147149

148-
### Canonicalization Algorithm Identifier
150+
### Canonicalization Algorithm Identifier**Shipped (alpha.3, all 4 langs)**
149151

150152
The current spec hardcodes the canonicalization algorithm (sorted keys, no whitespace, UTF-8). If the algorithm ever needs to change (and JSON canonicalization is notoriously tricky across languages), there's no way to signal which algorithm was used.
151153

@@ -156,7 +158,7 @@ The current spec hardcodes the canonicalization algorithm (sorted keys, no white
156158

157159
Trivial to add now, saves a painful migration later.
158160

159-
### A2A Context for Schema Verification
161+
### A2A Context for Schema Verification**Shipped (alpha.3, all 4 langs)**
160162

161163
When agents collaborate via A2A (Agent-to-Agent), tool schemas cross trust boundaries. SchemaPin v1.4.0 ensures that tool integrity verification extends seamlessly into A2A networks — every tool invoked through an A2A bridge is verified against its provider's signed schema.
162164

@@ -167,16 +169,21 @@ When agents collaborate via A2A (Agent-to-Agent), tool schemas cross trust bound
167169
| Domain scoping | Accept optional trusted domains as `Vec<String>`, matching the `AllowedDomains` type exported by AgentPin v0.3.0 (extracted from `AgentDeclaration.constraints`). Empty list means no domain restriction. |
168170
| Intersection check | Scope verification to intersection of caller's allowed domains and tool provider's domain |
169171

170-
### Trust Bundle Distribution for A2A Networks
172+
### Trust Bundle Distribution for A2A Networks**Shipped (alpha.4, all 4 langs)**
171173

172174
| Item | Details |
173175
|------|---------|
174-
| Bundle signing | Sign trust bundles with a bundle authority key |
176+
| Bundle signing | Sign trust bundles with a bundle authority key (ECDSA P-256 over `schemapin-v1` canonical bytes) |
175177
| `merge_trust_bundles()` | Combine bundles from multiple sources with deduplication (newest wins) |
176-
| TOFU for bundles | TOFU pinning for bundle authority keys |
177-
| JSON-RPC method | `schemapin/trustBundle` for A2A bundle exchange |
178+
| TOFU for bundles | TOFU pinning for bundle authority keys, keyed by `kid` |
179+
| JSON-RPC method | `schemapin/trustBundle` envelope helpers for A2A bundle exchange |
178180

179-
### Scan-Aware Signatures
181+
Bundle authority key carried as `public_key_pem` (consistent with discovery docs) rather than JWK — see the alpha.4 CHANGELOG for rationale.
182+
183+
### Scan-Aware Signatures — **Moved to v1.5.0**
184+
185+
> Descoped from v1.4.0 GA (2026-06-21). Additive and independent of the rest of
186+
> v1.4, so deferring it does not block GA. Spec below is retained as the v1.5 plan.
180187
181188
Right now scanning and signing are somewhat independent in the Symbiont/SchemaPin workflow. Making the scan result part of the signature metadata closes this gap — a skill signed with `scan_passed: true` at signing time, with the scanner version and ruleset hash recorded, tells verifiers not just that the content is authentic but that it passed security review at a specific rule version.
182189

@@ -200,7 +207,10 @@ Right now scanning and signing are somewhat independent in the Symbiont/SchemaPi
200207

201208
This is fully optional and backward compatible — v1.3 verifiers ignore the new fields.
202209

203-
### Cross-Agent Tool Schema Caching
210+
### Cross-Agent Tool Schema Caching — **Moved to v1.5.0**
211+
212+
> Descoped from v1.4.0 GA (2026-06-21). A local in-memory cache helper with no
213+
> wire-format impact; deferring it does not block GA. Spec below is the v1.5 plan.
204214
205215
| Item | Details |
206216
|------|---------|
@@ -216,6 +226,11 @@ All four language implementations (Rust, JavaScript, Python, Go) receive matchin
216226

217227
## v1.5.0 — Multi-Key Endorsement, Permissions & Advanced Revocation (Q4 2026)
218228

229+
**Carried over from v1.4 (descoped 2026-06-21):** Scan-Aware Signatures and
230+
Cross-Agent Tool Schema Caching. Both are additive and independent of the v1.4
231+
trust surface, so they ship in v1.5 rather than blocking the v1.4.0 GA. Their
232+
full specs live in the v1.4 section above (marked "Moved to v1.5.0").
233+
219234
### Multi-Key / Organizational Endorsement
220235

221236
The `.well-known/schemapin.json` should support an array of public keys with roles rather than a single `public_key_pem`. This is the enterprise compliance differentiator — organizations can enforce policies like "require both a developer and a reviewer signature."
@@ -324,4 +339,4 @@ We welcome input on roadmap priorities:
324339

325340
---
326341

327-
*Last updated: 2026-05-01 (v1.4.0-alpha.2schema version binding across all four language implementations)*
342+
*Last updated: 2026-06-21 (v1.4.0-alpha.4A2A trust-bundle distribution across all four language implementations; items 7–8 descoped to v1.5.0)*

TECHNICAL_SPECIFICATION.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,3 +588,50 @@ Implementations SHOULD copy these helpers from this section rather than taking a
588588
- v1.3 verifiers do not know about A2A context — they have no `verify_schema_for_a2a` entry point. Callers integrating with v1.3 verifiers fall back to the standard `verify_schema_offline` flow without scope enforcement.
589589
- The `A2A_SCOPE_VIOLATION` error code is new in v1.4. Older verifiers cannot emit it.
590590
- All v1.4 alpha.3 additions are additive — they introduce no changes to the signature on the wire (the new error code lives only in verifier output) so v1.4 alpha.1 / alpha.2 signatures verify identically under alpha.3 verifiers.
591+
592+
### **21. Trust Bundle Distribution (v1.4)**
593+
594+
#### **21.1. Purpose**
595+
596+
Allow a **bundle authority** to sign a trust bundle so it can be exchanged between agents over A2A without per-bundle out-of-band trust establishment. The receiving verifier authenticates the bundle and TOFU-pins the authority key by `kid`, the same trust model used for tool signing keys. All fields are optional and additive — an unsigned bundle is unchanged from v1.2/v1.3.
597+
598+
#### **21.2. Wire Format**
599+
600+
A signed trust bundle adds four OPTIONAL top-level fields:
601+
602+
```json
603+
{
604+
"schemapin_bundle_version": "1.4",
605+
"documents": [ "..." ],
606+
"revocations": [ "..." ],
607+
"bundle_authority": {
608+
"kid": "schemapin-bundle-authority-2026",
609+
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
610+
},
611+
"signed_at": "2026-05-15T00:00:00Z",
612+
"expires_at": "2099-01-01T00:00:00Z",
613+
"signature": "<base64 DER ECDSA P-256>"
614+
}
615+
```
616+
617+
The authority public key is carried as `public_key_pem` (consistent with discovery documents), making the bundle self-verifying. Signing sets `schemapin_bundle_version` to `"1.4"`.
618+
619+
#### **21.3. Signing Input**
620+
621+
The signature covers the `schemapin-v1` canonicalization (§19; recursive sorted keys, compact, UTF-8) of the entire bundle object with the `signature` field set to the empty string `""`. All four SDKs MUST produce the identical byte string so a bundle signed by any SDK verifies in every other.
622+
623+
#### **21.4. Operations**
624+
625+
- `sign_trust_bundle(bundle, private_key_pem, kid, signed_at, expires_at?)` — derive the authority public key, stamp `bundle_authority` / `signed_at` / `expires_at`, and write the signature.
626+
- `verify_trust_bundle(bundle, authority_pin_store)`:
627+
1. Require `bundle_authority` and `signature` — else `BUNDLE_UNSIGNED`.
628+
2. Reject when `expires_at` is present and in the past or unparseable — `BUNDLE_EXPIRED`.
629+
3. TOFU-pin the authority key fingerprint by `kid`; a different key under a pinned `kid` is `KEY_PIN_MISMATCH`.
630+
4. Verify the signature over the canonical bytes — failure is `SIGNATURE_INVALID`.
631+
- `merge_trust_bundles(bundles)` — deduplicate `documents` and `revocations` by domain, the newer source (`signed_at`, else `created_at`) winning. Returns an UNSIGNED bundle to be re-signed before redistribution.
632+
- `schemapin/trustBundle` JSON-RPC envelope helpers — `build_trust_bundle_request` / `build_trust_bundle_response` / `parse_trust_bundle_response`. The libraries define the message envelope; transport and the receiving pin-store update are the host application's responsibility.
633+
634+
#### **21.5. Backward Compatibility**
635+
636+
- Unsigned bundles omit all four fields and are byte-identical to pre-v1.4 bundles; existing resolvers ignore the new fields.
637+
- The `BUNDLE_UNSIGNED` and `BUNDLE_EXPIRED` error codes are new in v1.4.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ All four implementations use identical crypto (ECDSA P-256 + SHA-256) — cross-
8080
| [Signature Expiration](signature-expiration.md) | `expires_at` field for degraded-not-failed verification (v1.4-alpha, all 4 languages) |
8181
| [DNS TXT Cross-Verification](dns-txt.md) | Second-channel `_schemapin.{domain}` lookups (v1.4-alpha, all 4 languages) |
8282
| [Schema Version Binding](schema-version-binding.md) | `schema_version` + `previous_hash` lineage chain to defend against rug-pull substitutions (v1.4-alpha, all 4 languages) |
83+
| [Trust Bundle Distribution](trust-bundle-distribution.md) | Sign, verify, and merge trust bundles for safe A2A exchange (v1.4-alpha, all 4 languages) |
8384
| [Deployment](deployment.md) | Serve `.well-known` endpoints in production |
8485
| [Troubleshooting](troubleshooting.md) | Common issues and solutions |
8586

docs/trust-bundle-distribution.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Trust Bundle Distribution for A2A Networks
2+
3+
> **Status:** v1.4.0-alpha.4 — implemented in **Rust, Python, JavaScript, and Go**. A bundle signed by any one SDK verifies in every other (proven by the shared `tests/cross-language/signed_bundle.json` fixture).
4+
5+
A [trust bundle](trust-bundles.md) pre-packages discovery and revocation documents for offline verification. Until v1.4 a bundle had no authenticity of its own — you had to trust however it reached you. That is fine for a bundle you build and ship yourself, but not for one an agent hands to another agent over A2A.
6+
7+
v1.4 lets a **bundle authority** sign a trust bundle so it can be exchanged between agents without per-bundle out-of-band trust establishment. The receiving agent verifies the signature and TOFU-pins the authority key by `kid`, exactly as it would pin a tool's signing key.
8+
9+
All additions are optional fields — an unsigned bundle is byte-for-byte what it was before v1.4, and v1.2/v1.3 consumers ignore the new fields.
10+
11+
---
12+
13+
## Wire format
14+
15+
A signed bundle gains four optional top-level fields:
16+
17+
```json
18+
{
19+
"schemapin_bundle_version": "1.4",
20+
"created_at": "2026-05-15T00:00:00Z",
21+
"documents": [ /* ... */ ],
22+
"revocations": [ /* ... */ ],
23+
"bundle_authority": {
24+
"kid": "schemapin-bundle-authority-2026",
25+
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
26+
},
27+
"signed_at": "2026-05-15T00:00:00Z",
28+
"expires_at": "2099-01-01T00:00:00Z",
29+
"signature": "MEUCIQ..."
30+
}
31+
```
32+
33+
- **`bundle_authority`** — the authority that signed the bundle. The public key is carried as `public_key_pem` (consistent with discovery documents), so the bundle is self-verifying.
34+
- **`signed_at`** / **`expires_at`** — RFC 3339 timestamps. `expires_at` is optional even on a signed bundle; when present and past, verification fails with `BUNDLE_EXPIRED`.
35+
- **`signature`** — base64 DER ECDSA P-256 signature.
36+
37+
Signing stamps `schemapin_bundle_version` to `"1.4"`.
38+
39+
### Signing input
40+
41+
The signature covers the **`schemapin-v1` canonicalization** (recursive sorted keys, compact separators, UTF-8) of the entire bundle object with the `signature` field set to the empty string `""`. This is the same canonicalization used for schema and skill signing, so all four SDKs produce the identical byte string and cross-verify.
42+
43+
---
44+
45+
## Operations
46+
47+
| Operation | Purpose |
48+
|-----------|---------|
49+
| `sign_trust_bundle(bundle, private_key_pem, kid, signed_at, expires_at?)` | Stamp authority metadata and write the signature. Derives the authority public key from the private key. |
50+
| `verify_trust_bundle(bundle, authority_pin_store)` | Verify the signature, reject expired bundles, and TOFU-pin the authority key by `kid`. |
51+
| `merge_trust_bundles(bundles)` | Combine bundles from multiple sources, deduplicating by domain (newest wins). Returns an **unsigned** bundle to re-sign before redistribution. |
52+
| `build_trust_bundle_request` / `build_trust_bundle_response` / `parse_trust_bundle_response` | `schemapin/trustBundle` JSON-RPC envelope helpers for A2A exchange. |
53+
54+
(Function names are camel/Pascal-cased per language — e.g. `signTrustBundle` in JS, `SignTrustBundle` in Go.)
55+
56+
### Verification steps
57+
58+
`verify_trust_bundle` runs:
59+
60+
1. Require `bundle_authority` and `signature` — else `BUNDLE_UNSIGNED`.
61+
2. If `expires_at` is present and in the past (or unparseable) — `BUNDLE_EXPIRED`.
62+
3. TOFU-pin the authority key fingerprint by `kid`. A different key reusing a pinned `kid` — an impersonation attempt — fails with `KEY_PIN_MISMATCH`.
63+
4. Verify the signature over the canonical bytes — failure is `SIGNATURE_INVALID`.
64+
65+
---
66+
67+
## Example (Rust)
68+
69+
```rust
70+
use schemapin::bundle::{sign_trust_bundle, verify_trust_bundle, merge_trust_bundles};
71+
use schemapin::pinning::KeyPinStore;
72+
73+
// Authority signs a bundle for distribution.
74+
let signed = sign_trust_bundle(
75+
&bundle,
76+
&authority_private_pem,
77+
"schemapin-bundle-authority-2026",
78+
"2026-05-15T00:00:00Z",
79+
Some("2026-08-15T00:00:00Z"),
80+
)?;
81+
82+
// A receiving agent verifies it and TOFU-pins the authority.
83+
let mut authorities = KeyPinStore::new();
84+
verify_trust_bundle(&signed, &mut authorities)?;
85+
86+
// Combine bundles from several sources before re-signing.
87+
let merged = merge_trust_bundles(&[signed, other_signed]);
88+
```
89+
90+
The JSON-RPC helpers produce the `schemapin/trustBundle` message envelope; the
91+
transport and the receiving pin-store update are the host application's
92+
responsibility (e.g. a Symbiont coordinator receiving the message over A2A).
93+
94+
---
95+
96+
## Key rotation
97+
98+
The bundle authority is a long-lived signing key. To rotate it, sign new
99+
bundles under a new `kid` and distribute the new authority public key through
100+
the same channel you bootstrap any first-use key. Verifiers TOFU-pin per `kid`,
101+
so a new `kid` is a first-use pin, not a mismatch.
102+
103+
See [Technical specification](https://github.com/ThirdKeyAI/SchemaPin/blob/main/TECHNICAL_SPECIFICATION.md) for the normative definition.

go/internal/version/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
package version
33

44
// Version is set at build time via ldflags
5-
var Version = "1.4.0-alpha.3"
5+
var Version = "1.4.0-alpha.4"
66

77
// GetVersion returns the current version string
88
func GetVersion() string {

0 commit comments

Comments
 (0)