Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
52 changes: 52 additions & 0 deletions .github/workflows/compatibility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Runtime compatibility

on:
push:
branches: [main]
paths: ['rust/**', 'python/**', 'server/**', 'tests/**', '.github/workflows/compatibility.yml']
pull_request:
paths: ['rust/**', 'python/**', 'server/**', 'tests/**', '.github/workflows/compatibility.yml']

permissions:
contents: read

jobs:
rust:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust: ['1.89', stable]
defaults:
run:
working-directory: rust
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
toolchain: ${{ matrix.rust }}
components: rustfmt, clippy
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: rust
- run: cargo test --all-features --locked
- run: cargo fmt --check
if: matrix.rust == 'stable'
- run: cargo clippy --all-features --all-targets --locked -- -D warnings
if: matrix.rust == 'stable'

python:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ['3.10', '3.12']
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- run: python -m pip install --upgrade pip 'setuptools>=83.0.0' wheel
- run: python -m pip install -e './python[dev]' -r server/requirements.txt httpx pip-audit
- run: python -m pytest python/tests server/tests
- run: pip-audit --progress-spinner off
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,9 @@ yarn-error.log*

# Claude / AI tooling
.claude/
docs/superpowers/
docs/superpowers/
# Private keys must never be committed (SP-18). Test-only key material lives
# under tests/cross-language/keys/ and is listed explicitly there.
*_private.pem
*_private_key.pem
schemapin_private.pem
116 changes: 115 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,120 @@ All notable changes to the SchemaPin project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Supported runtimes

- Python package builds and CI use setuptools 83.0.0 or later to include its
security fixes.
- Python now requires 3.10 or later and Rust requires 1.89 or later, matching
the patched dependency graph. Packaging metadata and current documentation
agree on these minimums; CI tests each minimum alongside a newer runtime.

### Security

- Updated the pinned Python cryptography dependency and JavaScript development
dependency lockfile to clear current dependency advisories. The reference
server uses patched FastAPI, Starlette, AnyIO and Pydantic versions and no
longer installs unused JWT, password, multipart or file-helper libraries.

Fixes from the 2026-10 security review of all four SDKs, the reference server
and the demo. IDs refer to the review's findings. Wire-format changes are
called out explicitly; everything else is behavior.

- **SP-1** — Skill signatures now cover the whole `.schemapin.sig` document
(`schemapin_version: "1.4"`): `expires_at`, `schema_version`,
`previous_hash`, `canonicalization`, `domain`, `skill_name`, `signer_kid`,
`signed_at` and `file_manifest` are all signed. Verifiers recompute the root
hash, require `skill_hash` to match, and verify over the canonical document.
Legacy `"1.3"` documents still verify (with a `legacy_signature_format`
warning) but cannot anchor a lineage. **Wire-format change** for new
signatures.
- **SP-2** — Go signed the schema digest directly while Python, JavaScript and
Rust sign `ECDSA(SHA-256(digest))`; Go now matches. Go-produced schema and
skill signatures from earlier releases will not verify elsewhere (they never
did). A fixed cross-language ECDSA vector is committed.
- **SP-3 / SP-4 / SP-5** — `schemapin-v1` canonicalization is now specified
normatively (spec §4) and implemented identically: code-point key order,
literal `<>&` and U+2028/9, minimal escaping, ES6 number formatting with a
2^53−1 magnitude limit, rejection of `NaN`/`Infinity`, duplicate keys, lone
surrogates and invalid UTF-8, `__proto__` preserved. 33 shared vectors in
`tests/cross-language/canonicalization/` are asserted by all four suites.
Schemas containing floats such as `1.0` or integers above 2^53 canonicalize
differently than before (or are rejected).
- **SP-6** — Trust-bundle authorities must be allow-listed
(`seed_bundle_authority`); unknown authorities fail with
`BUNDLE_AUTHORITY_UNTRUSTED` unless first-use trust is explicitly allowed,
and the result reports `first_use`.
- **SP-7** — Reference server: bearer-token authentication on all mutations
(off by default), merge-on-upload that never wipes revocations, no CORS
credentials with wildcard origins, enforced rate limit, localhost/debug-off
defaults, host-based `/.well-known/schemapin.json` with no first-developer
fallback.
- **SP-8** — Skill verification binds the caller's domain: resolver paths fail
with `DOMAIN_MISMATCH` before any I/O when the signature's `domain` differs,
offline paths accept an expected domain, and the signature's own `domain`
is display-only.
- **SP-9** — Revocation is fail-closed everywhere: an unreachable
`revocation_endpoint` or `.well-known` document is `DISCOVERY_FETCH_FAILED`
(opt-out: `allow_unreachable_revocation`), `ChainResolver` propagates
errors, and the legacy workflows consult `revocation_endpoint`.
- **SP-10** — Only ECDSA P-256 keys are accepted in every SDK (load, sign,
verify, fingerprint).
- **SP-11** — Discovery transport: `https://` only, no redirects, 64 KiB body
cap, timeouts in every fetcher, and a shared RFC 1123 domain validator
applied before URLs, pin keys and file paths.
- **SP-12 / SP-13** — Skill canonicalization v2: symlinks and special files
abort hashing, per-entry hashes are length-prefixed
(`"f" ‖ be64(len(path)) ‖ path ‖ be64(len(content)) ‖ content`), the root
covers path/digest pairs, and entry count and total size are capped.
**Wire-format change** (selected by `schemapin_version: "1.4"`).
- **SP-14** — `skill_hash` is `hex(root_hash)` in all SDKs (Rust previously
hashed the root hash again) and is verified.
- **SP-15** — Legacy `KeyPinning` stores are keyed by `(tool_id, domain)` with
in-place migration; the JavaScript store fails hard on corruption, writes
atomically with mode 0600 and checks revocation in its interactive flows;
the legacy workflows pin only after a successful verification.
- **SP-16** — `LocalFileResolver` validates the domain and confines resolved
paths to the configured directory.
- **SP-17** — Expiration fails closed (`SIGNATURE_EXPIRED`, 5-minute skew,
strict RFC 3339); `allow_expired` restores the degraded behavior per call.
- **SP-18** — Private keys are written with mode 0600; committed demo and
fixture private keys were removed and the fixture bundle re-signed under the
test-only authority `schemapin-test-fixture-authority`.
- **SP-19** — Parity fixes: strict RFC 3339 parsing in JavaScript, Go rejects
an empty `canonicalization` field and trailing DER bytes, `go.mod` pins a
toolchain, bundle signing input is built from the known field set in every
SDK, duplicate bundle domains are rejected.
- **SP-20** — Documentation, CLI and demo drift fixed (see below).
- Pins are written only after signature verification in every verification
path; `KeyPinStore` gains `check` / `pin` / `seed`.
- Trust-bundle merges honor an authority precedence list and compare parsed
timestamps.

### Added

- `LineageStore` (all SDKs): verifier-side `latest_known_hash` per
`tool_id@domain` with fork/rollback and monotonic `schema_version`
enforcement (spec §18.4).
- `verify_schema_offline_with_dns` (all SDKs).
- Cross-language fixtures: canonicalization corpus, ECDSA signature vector,
v1.4 and legacy skill directories, re-signed trust bundle, and test-only keys
under `tests/cross-language/keys/`.
- Property-based tests (hypothesis, proptest, Go native fuzz seeds, a seeded
generator in JavaScript) and libFuzzer targets under `rust/fuzz/`.
- `server/tests/` for the reference server.

### Changed

- Dependency bumps: `h2`, `rustls`, `crossbeam-epoch`, `chacha20` (Rust,
optional features); Go toolchain pinned to go1.26.9.
- `KeyPinning` / `SchemaVerificationWorkflow` are deprecated in favor of
`KeyPinStore` + `verify_schema_with_resolver`.
- `rust`: `canonicalize_schema` / `canonicalize_and_hash` return `Result`;
`verify_trust_bundle` is strict (use `verify_trust_bundle_with_options`
for first-use trust).

## [1.4.0-alpha.4] - 2026-06-21

### Added
Expand Down Expand Up @@ -430,4 +544,4 @@ cross-protocol loop with AgentPin v0.3's `AllowedDomains` typed wrapper

---

For more details on any release, see the [GitHub releases page](https://github.com/thirdkey/schemapin/releases).
For more details on any release, see the [GitHub releases page](https://github.com/thirdkey/schemapin/releases).
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ SchemaPin lets tool developers sign their schemas and skill folders with ECDSA P
- **Skill folder signing** for AgentSkills (SKILL.md + file manifests)
- **Cross-language** — Python, JavaScript, Go, and Rust implementations

> **v1.4.0-alpha.2** (all four languages): three additive optional features — [signature expiration (`expires_at`)](https://docs.schemapin.org/signature-expiration/) with degraded-not-failed verification, [DNS TXT cross-verification](https://docs.schemapin.org/dns-txt/) at `_schemapin.{domain}` for second-channel trust, and [schema version binding (`schema_version` + `previous_hash`)](https://docs.schemapin.org/schema-version-binding/) for opt-in lineage chain enforcement that defends against rug-pull substitutions. v1.3 verifiers ignore the new fields; v1.4 verifiers handle both. The remaining v1.4 items (canonicalization id, A2A context, A2A trust bundles, scan-aware sigs, cross-agent schema cache) ship in subsequent alphas before stable v1.4.0.
> **v1.4.0-alpha.2** (all four languages): three additive optional features — [signature expiration (`expires_at`)](https://docs.schemapin.org/signature-expiration/) with fail-closed verification (`allow_expired` opt-out), [DNS TXT cross-verification](https://docs.schemapin.org/dns-txt/) at `_schemapin.{domain}` for second-channel trust, and [schema version binding (`schema_version` + `previous_hash`)](https://docs.schemapin.org/schema-version-binding/) for opt-in lineage chain enforcement that defends against rug-pull substitutions. v1.3 verifiers ignore the new fields; v1.4 verifiers handle both. The remaining v1.4 items (canonicalization id, A2A context, A2A trust bundles, scan-aware sigs, cross-agent schema cache) ship in subsequent alphas before stable v1.4.0.

## Quick Start

Expand All @@ -41,6 +41,8 @@ result = verifier.verify_schema(schema, signature, "example.com/my_tool", "examp

### Python

The upcoming security release requires Python 3.10 or later.

```bash
pip install schemapin
```
Expand All @@ -59,6 +61,8 @@ go install github.com/ThirdKeyAi/schemapin/go/cmd/...@latest

### Rust

The upcoming security release requires Rust 1.89 or later.

```toml
[dependencies]
schemapin = "1.3.0"
Expand Down
21 changes: 16 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@

We actively support the following versions of SchemaPin with security updates:

| Version | Supported |
| ------- | ------------------ |
| 1.1.x | :white_check_mark: |
| 1.0.x | :white_check_mark: |
| < 1.0 | :x: |
| Version | Supported |
| -------------- | ------------------ |
| 1.4.x (alpha) | :white_check_mark: |
| 1.3.x | :white_check_mark: |
| 1.2.x | :white_check_mark: |
| < 1.2 | :x: |

## Security Properties and Known Limitations

- **Fail-closed by default.** Expired skill signatures (`SIGNATURE_EXPIRED`), unreachable revocation sources (`DISCOVERY_FETCH_FAILED`), unknown trust-bundle authorities (`BUNDLE_AUTHORITY_UNTRUSTED`) and domain mismatches all fail verification. Each has an explicit, per-call opt-out (`allow_expired`, `allow_unreachable_revocation`, `allow_first_use`) that a host must choose deliberately.
- **Transport.** Discovery and revocation fetches are `https://` only, never follow redirects, cap the response body and apply timeouts. Domains are validated against the RFC 1123 hostname grammar before they are used as URLs, pin keys or file names.
- **Keys.** Only ECDSA P-256 keys are accepted anywhere. Private keys produced by the tooling are written with mode 0600; never commit them.
- **Pinning.** A pin is written only after a signature has verified, and pins are keyed by tool id *and* domain. Automatic first-use pinning is an opt-in; interactive or pre-seeded (`KeyPinStore.seed`, DNS TXT) trust is recommended.
- **Legacy skill signatures** (`schemapin_version: "1.3"`) cover only the root hash: `expires_at`, `schema_version`, `previous_hash`, `file_manifest` and the other metadata fields are not signed. They still verify, with a `legacy_signature_format` warning, but cannot anchor a lineage. Re-sign skills with a current SDK to get the signed document format.
- **Trust bundles** carry their own authority key. Seed the authority fingerprint out of band; treat `first_use` on a bundle exactly like a first-use tool key.
- **Reference server.** The server in `server/` is a reference implementation; its management API is disabled by default and requires a bearer token when enabled. Terminate TLS in front of it.

## Reporting a Vulnerability

Expand Down
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ Both features are **additive optional fields/records**: v1.3 verifiers ignore th

### Signature Expiration (`expires_at`)

Optional ISO 8601 / RFC 3339 timestamp on `.schemapin.sig`. Verifiers past the timestamp degrade the result (warning) instead of failing — `valid` stays `true`, `expired` becomes `true`, and a `signature_expired` warning is appended.
Optional RFC 3339 timestamp on `.schemapin.sig`, covered by the signature. Verifiers past the timestamp (plus 5 minutes of skew) fail with `SIGNATURE_EXPIRED`; with the explicit `allow_expired` opt-in the result degrades instead — `valid` stays `true`, `expired` becomes `true`, and a `signature_expired` warning is appended.

```rust
use schemapin::skill::{sign_skill_with_options, SignOptions, verify_skill_offline};
Expand Down
Loading
Loading