From 6453c1ff5922709651bbc218b4b4ece8a01ddbce Mon Sep 17 00:00:00 2001 From: Binay <150876063+bkd-dotcom@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:09:22 -0400 Subject: [PATCH 1/2] chore: relicense to BUSL-1.1 + publish the receipt spec (Signetry goes open core) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relicenses signetry-core from "All Rights Reserved" to BUSL-1.1, converting to Apache-2.0 on 2030-08-31, and publishes the receipt format as an Apache-2.0 specification with a conformance suite that is explicitly excluded from the BUSL Licensed Work. The spec is the substantive half. A receipt has to be verifiable by someone who does not have this tool and is reading it years later, so the format is an interface: docs/RECEIPT_SPEC.md writes it down (RFC 2119, threat model, change contract) and tests/conformance/ tests it with 7 committed language-agnostic JSON vectors and published test-key seeds. Two vectors carry the weight, because a signature-only verifier gets them wrong: resigned-other-key.json (a self-consistent envelope signed with the attacker's own keypair — verification must be against a pinned key, §8.1) and auto-merge-true.json (correctly signed, still non-conforming). So the verifier now checks conformance too: - check_invariants(receipt) returns RECEIPT_SPEC §4.3 violations - verify_receipt returns conforming/invariant_violations alongside the cryptographic result — separate questions, reported separately - signetry verify exits non-zero on a non-conforming receipt and never prints a bare VERIFIED when an invariant is broken The CLA's fallback grant changes from exclusive to non-exclusive: an exclusive fallback could be read as stripping a contributor of the right to use their own code, which was never the intent. .signetry/admission.yaml gains LICENSE* and install.sh so this change is admissible under core's own contract; evaluated empirically, status: pass. pytest: 298 passed (281 existing + 17 conformance). --- .github/workflows/cla.yml | 6 +- .github/workflows/release.yml | 12 +- .github/workflows/reviewer.yml | 2 +- .signetry/admission.yaml | 2 + CHANGELOG.md | 62 ++++ CLA.md | 44 ++- CONTRIBUTING.md | 79 ++++- CONTRIBUTORS.md | 19 +- LICENSE | 97 ++++++ LICENSE-Apache-2.0.txt | 202 ++++++++++++ README.md | 90 ++++-- SECURITY.md | 4 +- docs/INTEGRATIONS.md | 2 +- docs/LAUNCH.md | 8 +- docs/ORG_MIGRATION.md | 8 +- docs/RECEIPT_SPEC.md | 298 ++++++++++++++++++ docs/RELEASING.md | 13 +- docs/site/index.md | 35 +- docs/site/quickstart.md | 2 +- docs/site/scan-quickstart.md | 2 +- install.sh | 5 +- integrations/github-action/action.yml | 2 +- pyproject.toml | 2 +- signetry_core/cli.py | 11 +- signetry_core/pipeline/__init__.py | 2 + signetry_core/pipeline/receipt.py | 34 +- tests/conformance/README.md | 74 +++++ tests/conformance/generate_vectors.py | 170 ++++++++++ tests/conformance/test_receipt_conformance.py | 203 ++++++++++++ .../conformance/vectors/auto-merge-true.json | 30 ++ .../conformance/vectors/canonicalization.json | 19 ++ .../vectors/ephemeral-no-pinned-key.json | 30 ++ tests/conformance/vectors/keys.json | 11 + .../vectors/no-canonical-hash.json | 29 ++ .../vectors/resigned-other-key.json | 31 ++ .../conformance/vectors/tampered-payload.json | 31 ++ tests/conformance/vectors/valid.json | 33 ++ 37 files changed, 1615 insertions(+), 89 deletions(-) create mode 100644 LICENSE create mode 100644 LICENSE-Apache-2.0.txt create mode 100644 docs/RECEIPT_SPEC.md create mode 100644 tests/conformance/README.md create mode 100755 tests/conformance/generate_vectors.py create mode 100644 tests/conformance/test_receipt_conformance.py create mode 100644 tests/conformance/vectors/auto-merge-true.json create mode 100644 tests/conformance/vectors/canonicalization.json create mode 100644 tests/conformance/vectors/ephemeral-no-pinned-key.json create mode 100644 tests/conformance/vectors/keys.json create mode 100644 tests/conformance/vectors/no-canonical-hash.json create mode 100644 tests/conformance/vectors/resigned-other-key.json create mode 100644 tests/conformance/vectors/tampered-payload.json create mode 100644 tests/conformance/vectors/valid.json diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index f08b6c2..006b7ab 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -4,7 +4,9 @@ name: CLA # before their pull request can be merged. Runs entirely in this repo's Actions — # no third-party OAuth app. Signatures are recorded in signatures/cla.json on this # repo, so there is a durable, auditable record that each contributor assigned -# copyright/ownership of their contribution to the Owner. +# copyright in their contribution to the Owner — which is what lets code be +# relicensed across the open-core line (BUSL-1.1 engine <-> Apache-2.0 integrations) +# without re-asking every past contributor. # # A contributor signs by commenting the exact statement below on their PR: # I have read the CLA Document and I hereby sign the CLA @@ -48,7 +50,7 @@ jobs: path-to-document: "https://github.com/Signetry/core/blob/main/CLA.md" branch: "cla-signatures" allowlist: "dependabot[bot],bkd-dotcom" - custom-notsigned-prcomment: "Thanks for your contribution! Before it can be merged, please read the **[Contributor License Agreement](https://github.com/Signetry/core/blob/main/CLA.md)** — Signetry is **All Rights Reserved**, and by signing you assign copyright/ownership of your contribution to the Owner (you may not use, sell, or commercialize it yourself). To agree, reply with exactly:" + custom-notsigned-prcomment: "Thanks for your contribution! Before it can be merged, please read the **[Contributor License Agreement](https://github.com/Signetry/core/blob/main/CLA.md)** — Signetry is **open core** (this engine is BUSL-1.1, converting to Apache-2.0 on 2030-08-31; the integrations are Apache-2.0). Signing assigns copyright in your contribution to the Owner so code can be relicensed across that open-core line later without re-asking every contributor. It does not take away the rights the LICENSE already grants you. To agree, reply with exactly:" custom-pr-sign-comment: "I have read the CLA Document and I hereby sign the CLA" custom-allsigned-prcomment: "All contributors have signed the CLA. ✅" lock-pullrequest-aftermerge: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ceeaae4..6123a51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,12 +2,12 @@ name: Release # Cuts a GitHub Release when a version tag is pushed (e.g. v0.5.3). # -# signetry-core is SOURCE-AVAILABLE (All Rights Reserved) and is NOT published to -# PyPI — it is installed from source by tag: +# signetry-core is source-available under BUSL-1.1 (Apache-2.0 on 2030-08-31) and is +# not published to PyPI — it is installed from source by tag: # pip install "signetry-core @ git+https://github.com/Signetry/core@v0.5.3" -# The former PyPI Trusted-Publishing job was removed on the source-available -# lockdown (all PyPI releases were yanked). This workflow only builds/tests and -# attaches the built artifacts to a GitHub Release for convenience. +# The former PyPI Trusted-Publishing job was removed when distribution moved to +# git-by-tag (all prior PyPI releases were yanked). This workflow only builds/tests +# and attaches the built artifacts to a GitHub Release for convenience. # # Then: git tag v0.5.3 && git push origin v0.5.3 @@ -89,7 +89,7 @@ jobs: echo "Release ${GITHUB_REF_NAME}. See CHANGELOG.md." > notes.md fi echo "" >> notes.md - echo "Source-available (All Rights Reserved); install from source:" >> notes.md + echo "BUSL-1.1 (Apache-2.0 on 2030-08-31); install from source:" >> notes.md echo '```' >> notes.md echo "pip install \"signetry-core @ git+https://github.com/Signetry/core@${GITHUB_REF_NAME}\"" >> notes.md echo '```' >> notes.md diff --git a/.github/workflows/reviewer.yml b/.github/workflows/reviewer.yml index 72a0d33..ef2fc49 100644 --- a/.github/workflows/reviewer.yml +++ b/.github/workflows/reviewer.yml @@ -37,7 +37,7 @@ jobs: python-version: "3.12" - name: Install signetry-reviewer - # source-available (All Rights Reserved); install from source, not PyPI. + # not published to PyPI — install from its source repo by tag. run: pip install "signetry-reviewer @ git+https://github.com/Signetry/reviewer@v0.2.0" - name: Compute the PR diff diff --git a/.signetry/admission.yaml b/.signetry/admission.yaml index 970c947..bc17cf0 100644 --- a/.signetry/admission.yaml +++ b/.signetry/admission.yaml @@ -12,7 +12,9 @@ allowed_paths: - "docs/**" - ".signetry/**" - "*.md" + - "LICENSE*" - "pyproject.toml" + - "install.sh" - "Makefile" - "mkdocs.yml" - ".gitignore" diff --git a/CHANGELOG.md b/CHANGELOG.md index a334876..6884171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,68 @@ change between minor versions. ## [Unreleased] +### Added — the receipt format is now a published, independently testable spec + +- **[`docs/RECEIPT_SPEC.md`](docs/RECEIPT_SPEC.md)** documents the + `signetry.remediation-receipt` v1 format in full: envelope, payload, + canonicalization, signing, the verification algorithm, the §4.3 invariants, and a + change contract. RFC 2119 language throughout. A receipt is meant to be verifiable + by someone who does not have this tool and is reading it years later, so the format + is an interface and is now written down as one. +- **[`tests/conformance/`](tests/conformance/)** — 17 assertions over 7 committed JSON + vectors, with published test-key seed strings so an implementation in any language + can be checked against exactly the same files. Regenerate with + `python tests/conformance/generate_vectors.py`; the vectors are committed rather + than computed at test time so a change to canonicalization or signing shows up as a + diff. +- **The spec and the suite are [Apache-2.0](LICENSE-Apache-2.0.txt), named as explicit + exclusions from the BUSL `Licensed Work`** in [`LICENSE`](LICENSE). They carry no + restriction and no Change Date. Writing a competing issuer or an independent + verifier against the spec is a supported use. +- **`check_invariants(receipt)`** (exported from `signetry_core.pipeline`) enforces + RECEIPT_SPEC §4.3: `auto_merge` must be `false`, `human_review_required` must be + `true`, plus `kind`/`version`/`authority_level` well-formedness. `verify_receipt` + now returns `conforming` and `invariant_violations` alongside its cryptographic + result, because those are different questions — a receipt can be correctly signed + and still claim something the format forbids. +- **`signetry verify` now fails on a non-conforming receipt**, not just an unverifiable + one, and says which of the two failed. A validly signed receipt with + `auto_merge: true` prints `NON-CONFORMING` and `REJECTED` and exits `1`; it never + prints a bare `VERIFIED`. This is what makes "Signetry never merges on its own + judgement" a checkable property of every receipt instead of a promise in a README. + +### Fixed + +- Two repo-root-relative links in `docs/RELEASING.md` resolved from `docs/` and were + therefore broken. + +### Changed — licence: open core (BUSL-1.1, converting to Apache-2.0) + +- `signetry-core` is now licensed **[BUSL-1.1](LICENSE)** and converts to + **Apache-2.0 on 2030-08-31**, replacing the previous "All Rights Reserved" + proprietary terms. You may read, run in your own CI, use in production to govern + repositories you or your organization control, fork, modify, and redistribute it; + the one carve-out is offering it to third parties as a paid, competing hosted + service. `pyproject.toml`'s `license` field is now `BUSL-1.1`. +- The **integration surface is Apache-2.0**: the + [Action](https://github.com/Signetry/action), + [plugins](https://github.com/Signetry/plugins), + [pre-commit guard](https://github.com/Signetry/precommit), and + [eval suite](https://github.com/Signetry/eval). +- The **CLA still applies** — open core means code moves across the BUSL/Apache line, + and the assignment is what allows that relicensing without re-asking every past + contributor. `CLA.md`, `CONTRIBUTING.md`, and `CONTRIBUTORS.md` were rewritten for + the open-source posture; README/docs/workflow comments no longer claim the project + is "not open source" or "All Rights Reserved". +- No functional or API change. Distribution is unchanged: still installed from source + by tag, not published to PyPI. +- **The CLA's fallback licence grant is now non-exclusive.** It previously granted the + Owner an *exclusive* licence where copyright assignment is not permitted by law, which + would have stripped contributors of the right to use their own contribution — directly + contradicting the rights the LICENSE grants everyone. The CLA text is now identical + across all Signetry repositories (bar the engine/integration licence wording) so the + legal terms cannot drift per-repo again. See [CLA.md](CLA.md) §2–3. + ### Added — Python insecure-deserialisation coverage - `marshal.load(s)` and `shelve.open` now flagged (CWE-502) — both execute arbitrary diff --git a/CLA.md b/CLA.md index 07c0cbc..44f0300 100644 --- a/CLA.md +++ b/CLA.md @@ -1,6 +1,11 @@ # Signetry Contributor License Agreement (CLA) -**Copyright (c) 2026 Binay Dalai. All rights reserved.** +**Copyright (c) 2026 Binay Dalai.** This repository is the Signetry **engine**, licensed +under **[BUSL-1.1](LICENSE)** — source-available today, converting to +[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) on **2030-08-31** — as part of +Signetry's [open-core model](https://github.com/Signetry/signetry/blob/main/LICENSING.md). +This Agreement governs what You grant the Owner when You contribute; it does not reduce +the rights the LICENSE already gives You (and everyone else) in this code. Thank you for your interest in contributing to Signetry ("the Project"), owned by Binay Dalai ("the Owner"). This Contributor License Agreement ("Agreement") @@ -26,21 +31,38 @@ Your Contribution. You agree that the Owner is the sole and exclusive owner of t Contribution once merged into the Project. To the extent any rights cannot be assigned by law, You grant the Owner a -**perpetual, worldwide, exclusive, irrevocable, royalty-free, sublicensable, and +**perpetual, worldwide, non-exclusive, irrevocable, royalty-free, sublicensable, and transferable license** to use, reproduce, modify, prepare derivative works of, publicly display, publicly perform, distribute, **sell, and commercialize** Your Contribution, in whole or in part, in any form and for any purpose. -## 3. The Owner's exclusive rights +Nothing in this section removes Your own ability to use Your Contribution: once it is +released as part of this repository it is available to You, as to anyone, under +[BUSL-1.1](LICENSE) — and under Apache-2.0 from the Change Date — and You retain any +rights You independently hold in the underlying ideas and techniques. + +## 3. Why the Owner needs this (open core) You acknowledge and agree that: -- The Owner alone retains the right to **use, license, sell, and monetize** the - Project, including Your Contribution. -- You obtain **no right** to use, copy, modify, distribute, sell, or commercialize - the Project or Your Contribution for Your own personal or commercial purposes, - except as expressly permitted in writing by the Owner. -- The Project is **not open source** and is licensed "All Rights Reserved." +- **You keep the licence's rights.** This repository is released under + [BUSL-1.1](LICENSE), so You may read it, run it in your own CI, use it in production + to govern repositories You or Your organization control, fork it, patch it, and + publish those patches — including Your own Contribution — on exactly the same terms + as any other user, with no separate written permission. The single carve-out applies + to everyone equally: You may not offer this engine to third parties as a paid, + competing hosted service. On **2030-08-31** the licence becomes Apache-2.0 and even + that carve-out lapses. +- **The Owner may relicense.** The assignment above lets the Owner use, license, sell, + and monetize the Project, including Your Contribution, and release it under other + terms. +- **Code may move across the open-core line.** This repository is the BUSL-1.1 engine; + Signetry's integration surface (the GitHub Action, the editor and agent plugins, the + pre-commit guard, the eval suite, and the receipt specification) is Apache-2.0. A + Contribution accepted here may later be moved out to an Apache-2.0 repository — the + receipt spec and its conformance suite are deliberately Apache-2.0 for exactly that + reason — or an Apache-2.0 adapter may be promoted into this engine. The CLA is what + makes that possible without asking every past contributor again. ## 4. Recognition of Contributors (credit, not rights) @@ -50,8 +72,8 @@ of Your work. This recognition is **attribution only**. It does **not**: -- grant You any ownership, license, or right to use, copy, sell, sublicense, or - commercialize the Project or Your Contribution; +- grant You ownership of the Project, or any rights beyond those the Project's + licence already grants everyone; - entitle You to represent the Project, or any part of it, as Your own work, product, or property, or to market or sell it under Your own name or brand; or - create any partnership, employment, or revenue-sharing relationship with the Owner. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f91869..5e83760 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,66 @@ -# Contribution Agreement +# Contributing to signetry-core -By submitting a Pull Request to this repository, you agree to assign and transfer all copyright and ownership of your contributed code to the repository owner. The owner retains the exclusive right to monetize, use, and control the entire codebase. +`signetry-core` is the **engine** of Signetry's [open-core model](https://github.com/Signetry/signetry/blob/main/LICENSING.md). +It is licensed [BUSL-1.1](LICENSE) and converts to Apache-2.0 on **2030-08-31**; +the integration surface around it (the [Action](https://github.com/Signetry/action), +the [plugins](https://github.com/Signetry/plugins), the +[pre-commit guard](https://github.com/Signetry/precommit), the +[eval suite](https://github.com/Signetry/eval)) is Apache-2.0 today. -## Signing the CLA (required before merge) +## What the licence lets you do + +Without asking anyone, at no cost: read the source, run it in your own CI, use it in +production to govern changes to repositories you or your organization control, fork +it, patch it, and publish those patches. The single carve-out is offering +`signetry-core` to third parties as a paid, competing hosted service — change +admission, agent governance, or receipt issuance and verification as a service. See +[LICENSE](LICENSE) for the exact grant. + +## Getting started + +```bash +uv venv +uv pip install -e ".[dev]" # or: make install +uv run pytest # hermetic — no real agent invoked, no network +uv run ruff check . +``` + +`make test`, `make lint`, and `make build` wrap the same commands. To exercise the +prompt-injection defense end to end: + +```bash +uv run python demos/injection/demo.py # or: make verify-injection +``` + +Every PR runs the test suite on Python 3.11–3.13 (`uv sync --extra dev`, +`uv run ruff check .`, `uv run pytest -q`), a `signetry scan` SARIF check, the +injection demo, and signetry-core's own admission self-check. Match that locally and +CI should be green. + +🌱 **Where to start:** the +[good-first-issues board](https://github.com/Signetry/signetry/issues/10) +and [Discussions](https://github.com/Signetry/signetry/discussions). +Well-scoped areas in this repo: + +- **A new detection rule** — add a vuln class or language to + `signetry_core/pipeline/findings/` with a test in `tests/test_findings_engine.py`. +- **An executor adapter** — wire a new coding agent behind the `Executor` protocol + (`signetry_core/executors/`). +- **Docs / examples** — clarify the admission pipeline, hardening, or a recipe. + +Keep the core deterministic and honest: no model or network calls in the +deterministic pipeline (agent/LLM work stays behind the `Executor` protocol or the +optional classifier hook), `auto_merge` always false, authority earned from evidence, +and no overstating enforcement tiers or detection scope. The +[pull request template](.github/PULL_REQUEST_TEMPLATE.md) is the checklist. + +## Signing the CLA (still required before merge) + +Open source does **not** mean no CLA. Signetry is open **core**, so code legitimately +moves across the line between the BUSL-1.1 engine and the Apache-2.0 integration +surface — a well-built adapter may be promoted into the engine, and engine code may be +released under Apache-2.0 early or at the Change Date. The CLA is what lets us do that +relicensing without tracking down every past contributor for permission again. This is enforced by a bot. When you open a pull request, the **CLA Assistant** check will ask you to sign the [Contributor License Agreement](CLA.md). Reply on the PR @@ -13,13 +71,16 @@ I have read the CLA Document and I hereby sign the CLA ``` Your acceptance is recorded in `signatures/cla.json`. A PR **cannot be merged** until -the CLA is signed. Signetry is **not open source** (All Rights Reserved) — by signing -you assign copyright/ownership of your contribution to the Owner and gain no right to -use, sell, or commercialize it yourself. +the CLA is signed. ## Credit Contributors are **acknowledged** in [CONTRIBUTORS.md](CONTRIBUTORS.md), the Git -history, and release notes. This is attribution only — you may truthfully say you -contributed, but it grants no ownership and no right to use, sell, or rebrand the -project as your own. See the "Recognition of Contributors" clause in [CLA.md](CLA.md). +history, and release notes. See the "Recognition of Contributors" clause in +[CLA.md](CLA.md). + +## Conduct and security + +Be decent — see [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Security issues go through +[private reporting](https://github.com/Signetry/core/security/advisories/new), not a +public issue; see [SECURITY.md](SECURITY.md). diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7594090..cf7a71d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,14 +1,19 @@ # Contributors -Signetry is **owned by Binay Dalai** and is **All Rights Reserved** (not open source). +This repository is the Signetry **engine**, licensed under **[BUSL-1.1](LICENSE)** +(source-available; it becomes Apache-2.0 on 2030-08-31) and maintained by Binay Dalai as +part of Signetry's +[open-core model](https://github.com/Signetry/signetry/blob/main/LICENSING.md). The people below are gratefully acknowledged for contributions accepted under the [Contributor License Agreement](CLA.md). -**Credit, not ownership.** Being listed here recognizes a person's contribution. It -does **not** grant any right to use, copy, sell, sublicense, or commercialize the -project, and does **not** entitle anyone to present the project (in whole or in part) -as their own work, product, or brand. All such rights remain exclusively with the -owner. +**Credit, not ownership.** Being listed here recognizes a person's contribution. +Everyone — contributors included — already has the right to read, run, fork, and patch +this code under BUSL-1.1, and to use it in production to govern their own repositories. +Listing here does **not** additionally grant +ownership of the project, and does **not** entitle anyone to present the project (in +whole or in part) as their own work, product, or brand, or to use the Signetry name to +endorse their own products. ## Owner / maintainer @@ -19,4 +24,4 @@ owner. -- **Advait Varhade** ([@AdvaitVarhade](https://github.com/AdvaitVarhade)) — SSRF (CWE-918) detection rule for JavaScript/Node ([#73](https://github.com/Signetry/core/pull/73)); SSRF URL-argument resolution for keyword + positional calls, plus httpx/aiohttp coverage ([#89](https://github.com/Signetry/core/pull/89)); identified the Python insecure-deserialisation gaps — `marshal`, `shelve`, and an explicitly unsafe `yaml` Loader ([#87](https://github.com/Signetry/core/issues/87)) +_Be the first — open a PR, sign the CLA, and your name goes here._ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd48568 --- /dev/null +++ b/LICENSE @@ -0,0 +1,97 @@ +License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +Parameters + +Licensor: Binay Dalai +Licensed Work: signetry-core. The Licensed Work is (c) 2026 Binay Dalai. + EXCLUDED FROM THE LICENSED WORK, and instead licensed under + the Apache License, Version 2.0 (see LICENSE-Apache-2.0.txt): + the receipt format specification (docs/RECEIPT_SPEC.md) and + its conformance suite (tests/conformance/). A receipt must + remain verifiable without licensing anything from the + Licensor, so the format and the tests that define it carry + no restriction and are not subject to the Change Date. +Additional Use Grant: You may make production use of the Licensed Work, including + running it in your own CI and using it to govern changes to + repositories you or your organization control, provided Your + use does not include offering the Licensed Work to third + parties on a hosted or embedded basis as a competitive + offering. For purposes of this license: + + A "competitive offering" is a Product offered to third parties + on a paid basis, including through paid support arrangements, + that significantly overlaps with the capabilities of the + Licensor's paid version(s) of the Licensed Work -- in + particular, providing change admission, agent governance, or + receipt issuance and verification as a service. If Your Product + is not a competitive offering when You first make it generally + available, it will not become a competitive offering later due + to the Licensor releasing a new version of the Licensed Work + with additional capabilities. In addition, Products that are + not provided on a paid basis are not competitive. + + "Product" means software that is offered to end users to manage + in their own environments or offered as a service on a hosted + basis. + + "Embedded" means including the source code or executable code + from the Licensed Work in a competitive offering. "Embedded" + also means packaging the competitive offering in such a way + that the Licensed Work must be accessed or downloaded for the + competitive offering to operate. + + Hosting or using the Licensed Work for internal purposes within + an organization is not considered a competitive offering. The + Licensor considers your organization to include all of your + affiliates under common control. +Change Date: 2030-08-31 +Change License: Apache License, Version 2.0 + +For information about alternative licensing arrangements for the Licensed Work, +please contact the Licensor. + +Notice + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. diff --git a/LICENSE-Apache-2.0.txt b/LICENSE-Apache-2.0.txt new file mode 100644 index 0000000..118c83c --- /dev/null +++ b/LICENSE-Apache-2.0.txt @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Binay Dalai + + 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. + diff --git a/README.md b/README.md index 865e957..bee9c54 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,11 @@ --- -> **Copyright (c) 2026 Binay Dalai. All rights reserved.** -> This repository is strictly for viewing and contributing to the original project. You may not use, copy, modify, distribute, or commercialize this code for your own personal or commercial projects without explicit written permission. Only the original author retains the right to use and monetize this project. - - [![CI](https://github.com/Signetry/core/actions/workflows/ci.yml/badge.svg)](https://github.com/Signetry/core/actions/workflows/ci.yml) [![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Signetry%20Admission-purple?logo=github)](https://github.com/marketplace/actions/signetry-admission) [![Docs](https://img.shields.io/badge/docs-signetry--core-blue)](https://binaydalai.me/signetry-core/) -[![Source-available](https://img.shields.io/badge/source-available-informational.svg)](CLA.md) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome%20(CLA)-brightgreen.svg)](https://github.com/Signetry/signetry/issues/10) +[![License](https://img.shields.io/badge/license-BUSL--1.1%20%E2%86%92%20Apache--2.0-blue.svg)](LICENSE) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome%20(CLA)-brightgreen.svg)](CONTRIBUTING.md) **An agent-agnostic change-control plane for coding agents.** @@ -61,7 +57,7 @@ One core (`run_admission`), five checkpoints an agent's change must pass through — see [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md): ```bash -# Source-available (not on PyPI). Install from the source repo: +# Not on PyPI — install from the source repo: pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0" ``` @@ -239,6 +235,42 @@ Set `SIGNETRY_SIGNING_KEY` (base64 of >=32 raw bytes) for a stable production signing key; without it a deterministic dev key is used and every receipt is honestly flagged `key_ephemeral`. +## The receipt format is a specification, not an implementation detail + +A receipt has to be verifiable by someone who does not have this tool, does not trust +it, and is reading it years later. That makes the format an interface, so it is +written down and tested as one: + +- **[`docs/RECEIPT_SPEC.md`](docs/RECEIPT_SPEC.md)** — the v1 format: envelope, + payload, canonicalization, signing, the verification algorithm, and a change + contract. RFC 2119 language throughout. +- **[`tests/conformance/`](tests/conformance/)** — language-agnostic JSON vectors with + published test-key seeds. Any implementation, in any language, can be checked + against exactly these files. + +Both are **Apache-2.0** and excluded from this repository's BUSL licence. Writing a +competing issuer or an independent verifier is a supported use. + +Two of the vectors carry most of the weight, because they are the ones a +signature-only implementation gets wrong: + +| Vector | The mistake it catches | +|---|---| +| `resigned-other-key.json` | Verifying a signature against the public key carried **inside the same envelope**. The forger supplies both halves, so it always passes. Real verification is against a key you obtained some other way — [§8.1](docs/RECEIPT_SPEC.md#81-the-pinned-key-rule). | +| `auto-merge-true.json` | Treating a valid signature as a valid receipt. `auto_merge: false` and `human_review_required: true` are invariants **inside the signed payload**, so they cannot be flipped or dropped without breaking the signature — but only a verifier that checks them turns that into a guarantee. | + +The second one is why "Signetry never merges on its own judgement" is a checkable +property of every receipt rather than a sentence in a README: + +```bash +$ signetry verify receipt.json --public-key "$SIGNETRY_PUBLIC_KEY" +VERIFIED (issued_by_signetry=True, hash_matches=True) +NON-CONFORMING — auto_merge must be false (RECEIPT_SPEC §4.3) +REJECTED — signature is genuine, but this is not a conforming receipt. +$ echo $? +1 +``` + ## Prompt-injection defense (OWASP LLM01) Coding agents read repository text — `README.md`, `CLAUDE.md`, `.cursorrules`, @@ -357,13 +389,16 @@ by any `Executor`. As of **0.5.0** it also ships a layered SAST detection engine ## Contributing -**Source-available, PRs welcome.** The code is public to read, evaluate, and -contribute to — but it is **not open source**; it is All Rights Reserved and -contributions are accepted **only under the [Contribution Agreement](CONTRIBUTING.md) -/ [CLA](CLA.md)**: by submitting a PR you assign copyright and ownership of your -contribution to the owner, who alone may use and monetize the codebase. You are -**credited** in [CONTRIBUTORS.md](CONTRIBUTORS.md) and release notes, but gain no -right to use, sell, or rebrand it. If you don't agree, don't submit a PR. +**Open core, PRs welcome.** The engine is source-available under +[BUSL-1.1](LICENSE) (Apache-2.0 on 2030-08-31) and everything you plug into it — +the Action, the plugins, the pre-commit guard, the eval suite — is Apache-2.0. +Read it, run it, fork it, patch it, and send the patch back. + +Contributions are accepted under the [CLA](CLA.md), which is still required: it lets +a well-built contribution move across the open-core line later (engine → Apache-2.0 +integration surface, or the reverse) without re-asking every contributor for +permission. Contributors are credited in [CONTRIBUTORS.md](CONTRIBUTORS.md), the Git +history, and release notes. See [CONTRIBUTING.md](CONTRIBUTING.md) for the details. 🌱 **Where to start:** the [good-first-issues board](https://github.com/Signetry/signetry/issues/10) @@ -383,8 +418,25 @@ self-check. Read [CONTRIBUTING.md](CONTRIBUTING.md) and ## License -**Copyright (c) 2026 Binay Dalai. All rights reserved.** This code is not open -source. You may not use, copy, modify, distribute, or commercialize it for your own -personal or commercial purposes without explicit written permission from the author, -who alone retains the right to use and monetize this project. See the notice at the -top of this file and [CONTRIBUTING.md](CONTRIBUTING.md). +[BUSL-1.1](LICENSE) — source-available, and it becomes [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) on **2030-08-31**. + +**You may**, at no cost and without asking: read the source, run it in your own CI, +use it in production to govern changes to repositories you or your organization +control, fork it, patch it, and publish those patches. + +**You may not** offer `signetry-core` to third parties as a paid, competing hosted +service — change admission, agent governance, or receipt issuance and verification +as a service. That one carve-out is what funds the work. + +Everything you actually plug into — the [GitHub Action](https://github.com/Signetry/action), +the [editor and agent plugins](https://github.com/Signetry/plugins), the +[pre-commit guard](https://github.com/Signetry/precommit), the +[adversarial eval suite](https://github.com/Signetry/eval), and the +[receipt specification](docs/RECEIPT_SPEC.md) and its +[conformance suite](tests/conformance/) — is **Apache-2.0**, so an integration you +build is yours with no strings. The spec and the suite are named as explicit +exclusions from the BUSL `Licensed Work` in [`LICENSE`](LICENSE): a receipt has to +stay verifiable without licensing anything from us, so they carry no restriction and +no Change Date. See [LICENSING.md](https://github.com/Signetry/signetry/blob/main/LICENSING.md). + +Contributions are accepted under the [CLA](CLA.md). diff --git a/SECURITY.md b/SECURITY.md index 1f7f7e7..962070b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,8 +6,8 @@ signetry-core is a security tool, so we hold its own security to a high bar. Fixes land on the latest tagged release of the [source repo](https://github.com/Signetry/core/releases) (signetry-core is -source-available and installed from source — not published to PyPI). Always run the -latest. +source-available under BUSL-1.1 and installed from source — not published to PyPI). +Always run the latest. | Version | Supported | |---|---| diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md index 5dc258d..1258b2e 100644 --- a/docs/INTEGRATIONS.md +++ b/docs/INTEGRATIONS.md @@ -13,7 +13,7 @@ Codex, Cursor, Copilot, Devin, or a human, identically. ## 1. Python package from source (the foundation) ```bash -# source-available (All Rights Reserved); not on PyPI — install from source +# BUSL-1.1 (Apache-2.0 on 2030-08-31); not on PyPI — install from source pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0" ``` diff --git a/docs/LAUNCH.md b/docs/LAUNCH.md index f5e87a2..9f6547b 100644 --- a/docs/LAUNCH.md +++ b/docs/LAUNCH.md @@ -33,8 +33,9 @@ branch-PR) is granted — sealed in an Ed25519-signed receipt that maps to SLSA provenance. Make it a required status check and nothing merges without a receipt. `auto_merge` is always false — a human merges. -Agent-agnostic; **source-available** (All Rights Reserved — not open source), -installed from source (`pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0"`). +Agent-agnostic; **open core** — the engine is source-available under BUSL-1.1 +(Apache-2.0 on 2030-08-31) and every integration is Apache-2.0 — installed from +source (`pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0"`). On the GitHub Marketplace: https://github.com/marketplace/actions/signetry-admission @@ -98,7 +99,8 @@ https://github.com/Signetry/autofix-demo/pulls Also usable as a CLI (`pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0"` → `signetry admit`), a git pre-push hook, an MCP server, or a Python library. -Source-available (All Rights Reserved — not open source). +Open core: the engine is source-available under BUSL-1.1 (Apache-2.0 on 2030-08-31) +and every integration around it is Apache-2.0. Core: https://github.com/Signetry/core diff --git a/docs/ORG_MIGRATION.md b/docs/ORG_MIGRATION.md index a86fb2b..8bb6a9f 100644 --- a/docs/ORG_MIGRATION.md +++ b/docs/ORG_MIGRATION.md @@ -14,9 +14,9 @@ re-linking** on transfer. They were done in this order to avoid downtime: |---|---|---| | **GitHub Marketplace** (`signetry-action`) | Listing may unpublish/relink | Re-verified the Marketplace listing points at `Signetry/action` after transfer | -> Note: signetry-core is **source-available and not published to PyPI** (installed from -> source by tag). There is **no PyPI Trusted Publisher** to re-link — the -> `release.yml` workflow only cuts a GitHub Release. +> Note: signetry-core is **not published to PyPI** (installed from source by tag). +> There is **no PyPI Trusted Publisher** to re-link — the `release.yml` workflow only +> cuts a GitHub Release. GitHub **auto-redirects** old repo URLs (clones, links, `uses:` refs) after a transfer, so external consumers keep working — but the item above is not covered @@ -53,7 +53,7 @@ done 5. **Docs site** — GitHub Pages / custom domain on `Signetry/core`. 6. **Marketplace** — confirmed the `signetry-action` listing shows the new owner. 7. **Re-run a release** — tagged a patch (e.g. `v0.5.4`) to confirm the GitHub Release - automation works under the org (no PyPI publish — source-available/git-install). + automation works under the org (no PyPI publish — install from git by tag). ## Teams in the org diff --git a/docs/RECEIPT_SPEC.md b/docs/RECEIPT_SPEC.md new file mode 100644 index 0000000..2f5a11f --- /dev/null +++ b/docs/RECEIPT_SPEC.md @@ -0,0 +1,298 @@ +# Signetry Remediation Receipt — Specification v1 + +**Status:** stable · **Kind:** `signetry.remediation-receipt` · **Version:** `1` +**Licence:** [Apache-2.0](../LICENSE-Apache-2.0.txt) — this specification and its +conformance suite are explicitly excluded from the engine's BUSL-1.1 `Licensed Work` +(see [`LICENSE`](../LICENSE)), so they carry no restriction and no Change Date. + +--- + +## 0. Why this document exists + +A coding agent proposed a change to your repository. Later — in code review, in an +incident, in an audit two years from now — someone has to answer: + +1. **What was this agent allowed to do?** +2. **Did the checks actually run, or did something claim they did?** +3. **Can I detect if this record has been altered since?** + +A pull-request description cannot answer those questions, because the thing that +wrote the description is the thing under scrutiny. A **receipt** answers them with +evidence that is verifiable *without* trusting, running, or licensing the tool that +produced it. + +This specification is deliberately unencumbered. A receipt is meant to outlive the +tool that issued it: an auditor in 2032 must be able to verify a receipt written in +2026 with nothing but this document, a SHA-256 implementation, and an Ed25519 +verifier. If you want to write a competing issuer or an independent verifier, that is +a **supported** use, not a tolerated one. + +## 1. Terminology + +The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, SHOULD NOT, MAY, and OPTIONAL +are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +- **Issuer** — the party producing and signing a receipt. +- **Verifier** — any party checking a receipt. A verifier need not be the issuer and + MUST NOT be required to trust the issuer's software. +- **Envelope** — the outer object: the receipt plus its hash, signature and key material. +- **Receipt** — the signed payload: the accountability record itself. +- **Pinned key** — an Ed25519 public key the verifier obtained **out of band**, not + from the envelope. + +## 2. Threat model + +The receipt is designed against a specific adversary: **the agent, and the tooling +around it, may misreport what happened.** Concretely: + +| Attack | Defence in this spec | +|---|---| +| Agent claims checks passed when they never ran | `checks` and `baseline_checks` are inside the signed payload; §7 G2 passes only on evidence of real execution | +| Agent edits the receipt after the fact | Signature over canonical JSON (§5, §6); any byte change invalidates it | +| Attacker mints a plausible receipt with their own keypair | Verification is against a **pinned** key, never the embedded one (§8) | +| Attacker strips the signature and presents an unsigned record | A receipt with no `canonical_hash` or no valid signature is NOT verified (§8) | +| Issuer signs with a key whose seed is publicly known | `key_ephemeral` MUST be declared; §7 G3 fails on an ephemeral key | +| Change quietly grants itself merge rights | `auto_merge` and `human_review_required` are invariants *inside* the signed payload (§4.3) | +| Agent widens its own permissions mid-run | `contract` + `policy_hash` are signed; §7 G1 binds the change to a pre-bound plan | + +**Explicit non-goals.** A receipt does not prove the change is *correct*, that the +code is *secure*, or that a human read it. It proves what authority was earned, on +what evidence, under what policy — and that the record has not been altered. + +## 3. Envelope + +The envelope is a JSON object. REQUIRED members: + +| Member | Type | Meaning | +|---|---|---| +| `receipt` | object | The signed payload (§4). | +| `canonical_hash` | string | `"sha256:" + hex` of the canonical serialization of `receipt` (§5). | +| `signature` | string | Base64 Ed25519 signature over the canonical serialization (§6). | +| `public_key` | string | Base64 of the raw 32-byte Ed25519 public key. Informational only — a verifier MUST NOT trust it (§8). | +| `algorithm` | string | `"Ed25519"`. | +| `key_ephemeral` | boolean | `true` when signed with a key whose seed is not a managed secret. | + +OPTIONAL: + +| Member | Type | Meaning | +|---|---|---| +| `gates` | object | The G1/G2/G3 summary (§7). Computed over the envelope, so it is deliberately **outside** the signed payload — G3 depends on the signature itself. | + +## 4. Receipt payload + +### 4.1 Identity and provenance + +| Field | Type | Notes | +|---|---|---| +| `kind` | string | MUST be `"signetry.remediation-receipt"`. | +| `version` | number | MUST be `1` for this specification. | +| `generated_at` | string | RFC 3339 / ISO 8601 timestamp, UTC. | +| `repo` | string | Repository identifier. | +| `base_commit` | string \| null | The commit the change was computed against. | +| `executor` | string \| null | The agent that drafted the change (e.g. `codex-cli`, `claude-code`). | +| `model_identity` | object \| null | Model provenance as reported by the executor. | +| `provider_ledger` | object | Every external provider consulted during the run. | + +### 4.2 Authority and evidence + +| Field | Type | Notes | +|---|---|---| +| `contract` | object | The change contract that applied (§9). | +| `policy_hash` | string \| null | Hash of that contract, so the governing policy is pinned. | +| `contract_result` | object | Whether the change stayed inside the contract. | +| `trust_boundary` | object \| null | What untrusted repository text was quarantined before the agent read it. | +| `verifier` | object \| null | An independent verification of the result, not the agent's own claim. | +| `checks` | object \| null | The checks that actually ran, and their outcomes. | +| `baseline_checks` | object \| null | The same checks on the unmodified tree, so a change cannot earn authority by *weakening* an already-failing check. | +| `check_diagnosis` | object \| null | Why a check failed, when it did. | +| `context_manifest` | object \| null | What the agent was allowed to see. | +| `plan_capability_set` | object \| null | Capabilities bound *before* the run. | +| `plan_adherence` | object \| null | Whether the change stayed within that plan. | +| `proposed_change` | object \| null | Change summary. | +| `diff_hash` | string \| null | `"sha256:" + hex` of the exact diff, binding the change without embedding it. | +| `authority_level` | number | Earned level, `0`–`3` (§10). | +| `authority` | string | Symbolic authority, e.g. `branch_pr_only`. | + +### 4.3 Outcome and invariants + +| Field | Type | Notes | +|---|---|---| +| `human_decision` | string \| null | Recorded human disposition, if any. | +| `pr_url` | string \| null | Where the change was proposed. | +| `outcome` | string \| null | Human-readable summary. | +| `auto_merge` | boolean | **Invariant: MUST be `false`.** | +| `human_review_required` | boolean | **Invariant: MUST be `true`.** | + +The two invariants live *inside the signed payload* on purpose. They cannot be +quietly dropped or flipped without invalidating the signature — so "Signetry never +merges on its own judgement" is a checkable property of every receipt, not a promise +in a README. + +A verifier encountering `auto_merge: true` MUST treat the receipt as +**non-conforming**, regardless of signature validity. + +## 5. Canonicalization + +The signature and hash are computed over a canonical serialization of the `receipt` +object. An issuer MUST produce, and a verifier MUST recompute, exactly this form: + +1. JSON object keys sorted lexicographically, recursively (`sort_keys=true`). +2. No insignificant whitespace: item separator `,`, key separator `:`. +3. Unicode NOT escaped — emit characters directly (`ensure_ascii=false`), UTF-8 encoded. +4. Values that are not natively JSON-serializable are rendered as their string form. + +Reference (Python): + +```python +json.dumps(receipt, sort_keys=True, separators=(",", ":"), + default=str, ensure_ascii=False) +``` + +> **Implementer's note.** Rule 3 is the one that bites. A serializer that escapes +> non-ASCII by default (Python's `json.dumps` without `ensure_ascii=False`, Go's +> `encoding/json` for HTML-sensitive bytes) produces a *different byte string* and +> therefore a different hash. Compare against the test vectors in §11 before +> trusting your implementation. + +`canonical_hash` is then `"sha256:"` followed by the lowercase hex SHA-256 digest of +the UTF-8 bytes of that string. + +## 6. Signing + +The signature is Ed25519 ([RFC 8032](https://www.rfc-editor.org/rfc/rfc8032)) over the +**UTF-8 bytes of the canonical string** — not over the hash, and not over +pretty-printed JSON. It is base64-encoded (standard alphabet, with padding). + +Because the canonical payload binds `base_commit`, `diff_hash`, `checks`, +`verifier`, `contract`, and `model_identity`, signing transitively binds all of them. + +Issuers MUST declare `key_ephemeral: true` whenever the signing key is derived from a +value that is not a managed secret — for example a deterministic development +fallback. An ephemeral key produces a structurally valid signature that proves +nothing about provenance, and §7 G3 MUST fail for it. + +## 7. Proof gates + +`gates` distils the evidence into three questions, each reported as `pass`, `fail`, or +`unproven`. **A gate MUST NOT report `pass` on missing evidence** — absent evidence is +`unproven`, never green. + +| Gate | Question | Passes only when | +|---|---|---| +| **G1 — Capability integrity** | What was this agent allowed to do? | A plan was bound *before* the run and the change stayed within it; contract hash present. | +| **G2 — Behavioural authenticity** | Did the checks and sandbox actually run? | The contract's required checks genuinely ran under real isolation and passed. A host-restricted or unavailable sandbox does NOT pass — it reports honestly. | +| **G3 — Interaction auditability** | Is the history tamper-evident? | The signature verifies **and** the signing key is non-ephemeral. | + +Gates are computed over the envelope rather than inside the receipt because G3 +depends on the signature, which cannot exist inside the payload it signs. + +## 8. Verification algorithm + +A conforming verifier, given an envelope and OPTIONALLY a pinned public key: + +1. If `receipt` is absent or not an object, or `signature` is absent → **not verified**. +2. **Determine the key to verify against.** Use the pinned key if supplied. + If no pinned key is supplied *and* the issuing instance would sign with an + ephemeral key, the verifier MUST **refuse to verify** and say why. Verifying a + receipt against a key whose seed is public proves nothing, and returning "valid" + there would be actively misleading. +3. Recompute the canonical serialization of `receipt` (§5) and its SHA-256 (§5). +4. `hash_matches` = `canonical_hash` is present **and** equals the recomputed hash. + An envelope carrying no `canonical_hash` MUST NOT be treated as verified. +5. `signature_valid` = Ed25519 verification of `signature` over the canonical bytes + **against the pinned key** — never against `envelope.public_key`. +6. `verified` = `signature_valid` **AND** `hash_matches`. +7. Report `key_matches_pinned` = whether the embedded key equals the pinned key. This + is diagnostic: a mismatch means the receipt was issued by a different key than the + one you trust. +8. Reject as non-conforming if `auto_merge` is `true` or `human_review_required` is + `false` (§4.3). + +Verification MUST NOT raise on malformed input; it returns a negative result with a +reason. A verifier that throws on hostile input is a denial-of-service surface. + +### 8.1 The pinned-key rule + +This is the single most important rule in the specification, and the easiest to get +wrong: + +> **A verifier MUST NOT verify a receipt's signature against the public key contained +> in that same receipt.** + +An attacker can generate a keypair, mint an internally consistent envelope claiming +whatever authority they like, and embed their own public key. Every hash will match +and the signature will verify — against *their* key. Self-consistency is not +provenance. The key MUST come from somewhere the attacker does not control. + +## 9. Change contract + +The `contract` object records the policy in force. A v1 contract carries +`version`, `task_type`, `allowed_paths`, `forbidden_paths`, `max_files_changed`, +`required_checks`, `network`, and `authority_on_success`. + +A v2 contract MAY additionally carry a **capability graph**: `allowed_tools`, +`denied_bash`, `allowed_mcp`, `allowed_skills`. These are additive and may only +**restrict**. An empty or absent list means "no additional restriction from this +class" and MUST NOT be interpreted as a widening. Provenance fields +(`policy_owner`, `policy_version`, `policy_approved_at`) are OPTIONAL; when absent +the policy MUST be treated as unsigned and surfaced as such, never silently trusted. + +## 10. Authority ladder + +| Level | Meaning | +|---|---| +| **L0** | No authority. Analysis only. | +| **L1** | Advisory. May report, may not propose a change for merge. | +| **L2** | `branch_pr_only`. May open a branch and a pull request. **Never merges.** | +| **L3** | Reserved for higher authority; requires evidence beyond L2 and is not granted by the default pipeline. | + +Authority is *earned on evidence*, never asserted. A change that weakens a +previously-passing check MUST NOT earn a higher level than one that does not. + +## 11. Conformance + +An implementation is **conforming** if it satisfies every MUST here and reproduces the +test vectors in `tests/conformance/` — which include: + +- a canonicalization vector with non-ASCII content and keys deliberately out of order +- a valid envelope that MUST verify against a supplied pinned key +- the same envelope with one byte of `receipt` changed, which MUST NOT verify +- an envelope whose `signature` was re-signed with a different key, which MUST NOT + verify against the pinned key (§8.1) +- an envelope with `canonical_hash` removed, which MUST NOT be treated as verified +- an ephemeral-key envelope with no pinned key, which MUST be refused rather than + reported valid +- an envelope with `auto_merge: true`, which MUST be rejected as non-conforming + +The vectors are language-agnostic JSON with published test-key seeds, and +`tests/conformance/` is **Apache-2.0 even though the engine is BUSL-1.1** — an +independent verifier in any language can be checked against exactly those files. See +[`tests/conformance/README.md`](../tests/conformance/README.md) for the per-vector +table and the key-derivation strings. + +Run the suite against the reference implementation: + +``` +python -m pytest tests/conformance/ -q +``` + +Or check a single receipt from the command line: + +``` +signetry verify --public-key +``` + +`signetry verify` exits non-zero when the signature does not verify against the +supplied key **or** when the payload breaks a §4.3 invariant, and prints which of the +two failed. A receipt that is correctly signed but non-conforming reports both — +never a bare `VERIFIED`. + +## 12. Versioning + +`version` is incremented only for a change that would make an existing conforming +verifier reject a valid receipt. Adding an OPTIONAL field is not such a change, so +verifiers MUST ignore unknown members rather than reject them. A verifier +encountering a `version` it does not implement MUST report that clearly instead of +guessing. + +`kind` is stable and MUST NOT be reused for an incompatible record type. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 307eb93..1ef3dec 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,20 +1,21 @@ # Releasing signetry-core -signetry-core is **source-available** (All Rights Reserved) and is **not published to -PyPI** — it is distributed and installed **from source by tag**: +signetry-core is source-available under **[BUSL-1.1](../LICENSE)** (it becomes +Apache-2.0 on 2030-08-31) and is **not published to PyPI** — it is distributed and +installed **from source by tag**: ```bash pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0" ``` -Pushing a version tag runs [`.github/workflows/release.yml`](.github/workflows/release.yml), +Pushing a version tag runs [`.github/workflows/release.yml`](../.github/workflows/release.yml), which verifies + tests + builds the artifacts and cuts a **GitHub Release** (the -former PyPI Trusted-Publishing job was removed on the source-available lockdown, as -all prior PyPI releases were yanked). +former PyPI Trusted-Publishing job was removed when distribution moved to git-by-tag, +as all prior PyPI releases were yanked). ## Cutting a release -1. Bump the version in [`pyproject.toml`](pyproject.toml) (`[project].version`). +1. Bump the version in [`pyproject.toml`](../pyproject.toml) (`[project].version`). 2. Commit: `git commit -am "release: v0.5.3"`. 3. Tag and push: ```bash diff --git a/docs/site/index.md b/docs/site/index.md index 19ba5b5..99f00e4 100644 --- a/docs/site/index.md +++ b/docs/site/index.md @@ -34,7 +34,7 @@ false — a human merges. ## Install ```bash -# source-available (All Rights Reserved); not on PyPI — install from source +# BUSL-1.1 (Apache-2.0 on 2030-08-31); not on PyPI — install from source pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0" ``` @@ -43,14 +43,29 @@ pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0" ## License & contributing -signetry-core is **source-available** — the code is public to read, evaluate, and -contribute to, but it is **not open source**. It is **All Rights Reserved -(© 2026 Binay Dalai)** and installed from source (not PyPI). +[BUSL-1.1](https://github.com/Signetry/core/blob/main/LICENSE) — source-available, and +it becomes [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) on +**2030-08-31**. -Contributions are welcome under a **Contributor License Agreement**: you can -contribute and you'll be **credited** (in `CONTRIBUTORS.md`, the Git history, and -release notes), but you gain no right to use, sell, or rebrand the project — the -owner retains all rights. See -[CONTRIBUTING.md](https://github.com/Signetry/core/blob/main/CONTRIBUTING.md), -the [CLA](https://github.com/Signetry/core/blob/main/CLA.md), and the +**You may**, at no cost and without asking: read the source, run it in your own CI, +use it in production to govern changes to repositories you or your organization +control, fork it, patch it, and publish those patches. + +**You may not** offer `signetry-core` to third parties as a paid, competing hosted +service — change admission, agent governance, or receipt issuance and verification +as a service. That one carve-out is what funds the work. + +Everything you actually plug into — the [GitHub Action](https://github.com/Signetry/action), +the [editor and agent plugins](https://github.com/Signetry/plugins), the +[pre-commit guard](https://github.com/Signetry/precommit), and the +[adversarial eval suite](https://github.com/Signetry/eval) — is **Apache-2.0**, so an +integration you build is yours with no strings. See +[LICENSING.md](https://github.com/Signetry/signetry/blob/main/LICENSING.md). + +Contributions are accepted under the +[CLA](https://github.com/Signetry/core/blob/main/CLA.md), which is still required: it +lets a contribution move across the open-core line later without re-asking every +contributor for permission. Contributors are **credited** in `CONTRIBUTORS.md`, the Git +history, and release notes. See +[CONTRIBUTING.md](https://github.com/Signetry/core/blob/main/CONTRIBUTING.md) and the [good-first-issues board](https://github.com/Signetry/signetry/issues/10). diff --git a/docs/site/quickstart.md b/docs/site/quickstart.md index 28755c8..ca4e81c 100644 --- a/docs/site/quickstart.md +++ b/docs/site/quickstart.md @@ -3,7 +3,7 @@ ## Install ```bash -# source-available (All Rights Reserved); not on PyPI — install from source +# BUSL-1.1 (Apache-2.0 on 2030-08-31); not on PyPI — install from source pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0" signetry completion zsh >> ~/.zshrc # optional: shell completion (bash | zsh | fish) ``` diff --git a/docs/site/scan-quickstart.md b/docs/site/scan-quickstart.md index 602def1..0e385f6 100644 --- a/docs/site/scan-quickstart.md +++ b/docs/site/scan-quickstart.md @@ -9,7 +9,7 @@ proving it — start at [Quickstart](quickstart.md). ## Install ```bash -# source-available (All Rights Reserved); not on PyPI — install from source +# BUSL-1.1 (Apache-2.0 on 2030-08-31); not on PyPI — install from source pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0" ``` diff --git a/install.sh b/install.sh index 5038c87..7ab1b9b 100755 --- a/install.sh +++ b/install.sh @@ -6,8 +6,9 @@ # # Installs the `signetry` CLI (signetry-core) from its **source repo** using the best # tool available, preferring an isolated install so it never clobbers your project -# environments. signetry-core is source-available (All Rights Reserved) and is NOT -# published to PyPI, so all installs pull from git by tag: +# environments. signetry-core is source-available under BUSL-1.1 (it becomes +# Apache-2.0 on 2030-08-31) and is not published to PyPI, so all installs pull from +# git by tag: # # 1. uv (uv tool install ...) — fastest, isolated # 2. pipx (pipx install ...) — isolated diff --git a/integrations/github-action/action.yml b/integrations/github-action/action.yml index 80d07b1..c4dad8a 100644 --- a/integrations/github-action/action.yml +++ b/integrations/github-action/action.yml @@ -45,7 +45,7 @@ runs: - name: Install signetry-core shell: bash - # source-available (All Rights Reserved); not on PyPI — install from source. + # BUSL-1.1 (Apache-2.0 on 2030-08-31); not on PyPI — install from source. run: pip install "signetry-core @ git+https://github.com/Signetry/core@v0.7.0" - name: Stage the PR change as a working-tree diff diff --git a/pyproject.toml b/pyproject.toml index 79ce0c6..3c3783e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.7.0" description = "An agent-agnostic change-control plane for coding agents. Governs Codex, Claude Code, Cursor, or any agent behind one admission pipeline and proves every change with a signed receipt." readme = "README.md" requires-python = ">=3.11" -license = { text = "Proprietary — All Rights Reserved" } +license = { text = "BUSL-1.1" } authors = [{ name = "Binay Dalai" }] keywords = [ "ai-agents", "coding-agents", "governance", "supply-chain", "slsa", diff --git a/signetry_core/cli.py b/signetry_core/cli.py index 74f52ef..f0a68e6 100644 --- a/signetry_core/cli.py +++ b/signetry_core/cli.py @@ -215,6 +215,9 @@ def cmd_verify(args: argparse.Namespace) -> int: print(f"error: cannot read receipt: {exc}", file=sys.stderr) return 2 result = verify_receipt(envelope, expected_public_key=args.public_key) + # Two independent checks: is the signature genuine, and does the payload obey + # the format's invariants (RECEIPT_SPEC §4.3)? A receipt needs both. + conforming = result.get("conforming", True) if args.json: _print(result, True) else: @@ -223,7 +226,13 @@ def cmd_verify(args: argparse.Namespace) -> int: print("NOT VERIFIED — " + result["reason"]) else: print(("VERIFIED" if ok else "NOT VERIFIED") + f" (issued_by_signetry={result['issued_by_signetry']}, hash_matches={result['hash_matches']})") - return 0 if result["verified"] else 1 + for violation in result.get("invariant_violations") or []: + print(f"NON-CONFORMING — {violation}") + if ok and not conforming: + # Don't let a reader stop at "VERIFIED": the signature is genuine and + # the receipt is still not usable as proof of anything. + print("REJECTED — signature is genuine, but this is not a conforming receipt.") + return 0 if (result["verified"] and conforming) else 1 def cmd_brake(args: argparse.Namespace) -> int: diff --git a/signetry_core/pipeline/__init__.py b/signetry_core/pipeline/__init__.py index da99fdf..bf5e0a6 100644 --- a/signetry_core/pipeline/__init__.py +++ b/signetry_core/pipeline/__init__.py @@ -57,6 +57,7 @@ ) from .receipt import ( build_receipt, + check_invariants, public_key_b64, sign, signing_key_is_ephemeral, @@ -134,6 +135,7 @@ "verify_change", "masked_recheck", "build_receipt", + "check_invariants", "verify_receipt", "verify_signature", "sign", diff --git a/signetry_core/pipeline/receipt.py b/signetry_core/pipeline/receipt.py index 998cb47..37ef09d 100644 --- a/signetry_core/pipeline/receipt.py +++ b/signetry_core/pipeline/receipt.py @@ -185,6 +185,29 @@ def build_receipt( return envelope +def check_invariants(receipt: dict[str, Any]) -> list[str]: + """Return the RECEIPT_SPEC §4.3 invariant violations in ``receipt`` (empty == conforming). + + These are separate from signature validity on purpose. A receipt can be + perfectly signed and still non-conforming: an issuer that sets + ``auto_merge: true`` has signed a claim the format does not permit. Both + checks must pass before a receipt means anything. + """ + violations: list[str] = [] + if receipt.get("kind") != "signetry.remediation-receipt": + violations.append(f"kind must be 'signetry.remediation-receipt' (got {receipt.get('kind')!r})") + if receipt.get("version") != 1: + violations.append(f"version must be 1 (got {receipt.get('version')!r})") + if receipt.get("auto_merge") is not False: + violations.append("auto_merge must be false (RECEIPT_SPEC §4.3)") + if receipt.get("human_review_required") is not True: + violations.append("human_review_required must be true (RECEIPT_SPEC §4.3)") + level = receipt.get("authority_level") + if not isinstance(level, int) or isinstance(level, bool) or not 0 <= level <= 3: + violations.append(f"authority_level must be an integer 0-3 (got {level!r})") + return violations + + def verify_receipt(envelope: dict[str, Any], *, expected_public_key: str | None = None) -> dict[str, Any]: """Independently verify a signed receipt envelope. @@ -206,13 +229,17 @@ def verify_receipt(envelope: dict[str, Any], *, expected_public_key: str | None embedded_key = envelope.get("public_key") if not isinstance(receipt, dict) or not signature: return {"verified": False, "hash_matches": False, "signature_valid": False, - "issued_by_signetry": False, "reason": "Receipt or signature missing."} + "issued_by_signetry": False, "conforming": False, + "invariant_violations": ["receipt or signature missing"], + "reason": "Receipt or signature missing."} # Fail closed when the pinned key would be the public dev-fallback key. if expected_public_key is None and signing_key_is_ephemeral(): return { "verified": False, "hash_matches": False, "signature_valid": False, "issued_by_signetry": False, "key_ephemeral": True, + "conforming": not check_invariants(receipt), + "invariant_violations": check_invariants(receipt), "reason": ( "Refusing to verify against the dev-fallback key (its seed is public). " "Set a production SIGNETRY_SIGNING_KEY, or pass expected_public_key/--public-key " @@ -221,6 +248,7 @@ def verify_receipt(envelope: dict[str, Any], *, expected_public_key: str | None } pinned_key = expected_public_key or public_key_b64() + violations = check_invariants(receipt) canonical = _canonical(receipt) computed_hash = _sha256(canonical) hash_matches = bool(claimed_hash) and claimed_hash == computed_hash @@ -241,4 +269,8 @@ def verify_receipt(envelope: dict[str, Any], *, expected_public_key: str | None "expected_public_key": pinned_key, "algorithm": envelope.get("algorithm", "Ed25519"), "key_ephemeral": envelope.get("key_ephemeral"), + # Conformance is reported separately from cryptography: a validly signed + # receipt that breaks a §4.3 invariant is still non-conforming. + "conforming": not violations, + "invariant_violations": violations, } diff --git a/tests/conformance/README.md b/tests/conformance/README.md new file mode 100644 index 0000000..a97c634 --- /dev/null +++ b/tests/conformance/README.md @@ -0,0 +1,74 @@ +# Receipt conformance suite + +Test vectors and assertions for the +[Signetry Remediation Receipt format](../../docs/RECEIPT_SPEC.md) (`version: 1`). + +**This directory and `docs/RECEIPT_SPEC.md` are licensed +[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0), not BUSL-1.1 like the rest +of this repository.** That is deliberate. A receipt is meant to outlive the tool that +issued it — an auditor in 2032 should be able to verify a receipt written in 2026 +without licensing anything from us. Writing a competing issuer or an independent +verifier against this spec is a supported use, not a tolerated one. + +## Running it + +```bash +python -m pytest tests/conformance/ -q +``` + +To regenerate the vectors after an intentional format change: + +```bash +python tests/conformance/generate_vectors.py +``` + +The vectors are **committed, not generated at test time**. A suite that recomputes +its own expected values cannot notice a change in how those values are computed; +committed vectors turn any change to canonicalization or signing into a visible diff +— which is what [RECEIPT_SPEC §9](../../docs/RECEIPT_SPEC.md#9-change-contract) +requires. If a diff appears in `vectors/` and you did not intend a format change, +something regressed. + +## Using it from another language + +The vectors are plain JSON and the two test keys are derived from published seed +strings, so any implementation can be checked against exactly these files: + +``` +key_A seed = SHA-256("signetry-conformance-vector-key-A") # 32 raw bytes +key_B seed = SHA-256("signetry-conformance-vector-key-B") +``` + +These are **test keys**. Their seeds are public on purpose. Never sign anything real +with them. + +Each vector has an `expect` block (or, for `canonicalization.json`, the expected +output directly) and a `_note` explaining what it proves. A conforming +implementation reproduces every `expect`. + +## The vectors + +| Vector | What it proves | +|---|---| +| `canonicalization.json` | Canonical bytes are exact: recursive key sort, no whitespace, **raw Unicode**. The usual failure is a serializer that escapes non-ASCII by default — Python's `json.dumps` does, hence `ensure_ascii=False`. | +| `valid.json` | The happy path verifies against a pinned key. | +| `tampered-payload.json` | One field edited after signing (`authority_level` 2 → 3) fails both the hash and the signature. | +| `resigned-other-key.json` | **The load-bearing one.** A self-consistent envelope signed with the attacker's own keypair, embedded key swapped to match. Every internal check passes. It MUST still fail against the pinned key — see [§8.1](../../docs/RECEIPT_SPEC.md#81-the-pinned-key-rule). | +| `no-canonical-hash.json` | A genuine signature with the hash stripped is **not** verified. Absence of evidence is not evidence. | +| `ephemeral-no-pinned-key.json` | A dev-key receipt with no key to pin against must be **refused**, not reported green. The dev seed is in this source tree; verifying against it proves nothing. | +| `auto-merge-true.json` | A correctly signed receipt claiming `auto_merge: true` is **non-conforming**. Signature validity and format conformance are separate questions. | + +## Why two of these matter more than the rest + +An implementation that only checks cryptography passes most of this suite. It fails +`resigned-other-key.json` and `auto-merge-true.json`, and those two are where the +format's guarantees actually live: + +- **Pin the key.** Verifying a signature against the public key carried inside the + same document is a no-op — the attacker supplies both halves. Every real + verification is against a key you obtained some other way. +- **A valid signature is not a valid receipt.** `auto_merge: false` and + `human_review_required: true` are invariants *inside the signed payload*, so they + cannot be flipped or dropped without breaking the signature. That makes "Signetry + never merges on its own judgement" a checkable property of every receipt rather + than a promise in a README — but only if the verifier actually checks it. diff --git a/tests/conformance/generate_vectors.py b/tests/conformance/generate_vectors.py new file mode 100755 index 0000000..0d13120 --- /dev/null +++ b/tests/conformance/generate_vectors.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Regenerate the RECEIPT_SPEC conformance vectors in ``vectors/``. + +Run from the repository root:: + + python tests/conformance/generate_vectors.py + +The vectors are committed rather than generated at test time on purpose: a +conformance suite that computes its own expected values cannot detect a change in +how those values are computed. Committed vectors turn any change to the +canonicalization or signing rules into a visible diff, which is exactly what +RECEIPT_SPEC §9's change contract requires. + +The two test keys are derived from published seed strings so an independent +implementation can regenerate these files byte for byte. They are TEST keys. Do +not sign anything real with them. +""" +from __future__ import annotations + +import base64 +import hashlib +import json +from pathlib import Path + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +OUT = Path(__file__).parent / "vectors" + +SEED_LABEL_A = b"signetry-conformance-vector-key-A" +SEED_LABEL_B = b"signetry-conformance-vector-key-B" + + +def canonical(payload: dict) -> str: + # Must match signetry_core.pipeline.receipt._canonical exactly. Duplicated + # here, not imported, so the vectors are not silently redefined by a change to + # the implementation — see RECEIPT_SPEC §5. + return json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str, ensure_ascii=False) + + +def sha256_tagged(text: str) -> str: + return "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest() + + +def keypair(label: bytes) -> tuple[Ed25519PrivateKey, str]: + key = Ed25519PrivateKey.from_private_bytes(hashlib.sha256(label).digest()) + raw = key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + return key, base64.b64encode(raw).decode() + + +KEY_A, PUB_A = keypair(SEED_LABEL_A) +KEY_B, PUB_B = keypair(SEED_LABEL_B) + +RECEIPT = { + "kind": "signetry.remediation-receipt", + "version": 1, + "generated_at": "2026-08-31T12:00:00+00:00", + "repo": "Signetry/conformance", + "base_commit": "0" * 40, + "executor": "none", + "policy_hash": "sha256:" + "a" * 64, + "diff_hash": "sha256:" + "b" * 64, + "authority_level": 2, + "authority": "branch_pr_only", + "outcome": "Conformance fixture — not a real remediation.", + "auto_merge": False, + "human_review_required": True, +} + + +def envelope(receipt: dict, key: Ed25519PrivateKey, public_key: str, *, + drop_hash: bool = False, ephemeral: bool = False) -> dict: + text = canonical(receipt) + env = { + "receipt": receipt, + "canonical_hash": sha256_tagged(text), + "signature": base64.b64encode(key.sign(text.encode("utf-8"))).decode(), + "public_key": public_key, + "algorithm": "Ed25519", + "key_ephemeral": ephemeral, + } + if drop_hash: + del env["canonical_hash"] + return env + + +def write(name: str, obj: dict, note: str) -> None: + obj = {**obj, "_note": note} + (OUT / name).write_text(json.dumps(obj, indent=2, ensure_ascii=False) + "\n") + print(f" {name}") + + +def main() -> None: + OUT.mkdir(parents=True, exist_ok=True) + + canon_payload = { + "zeta": 1, + "alpha": {"nested_z": True, "nested_a": [3, 2, 1]}, + "unicode": "receipt — Ünïcödé ✓ 日本語", + "empty": {}, + "null": None, + } + text = canonical(canon_payload) + write("canonicalization.json", { + "payload": canon_payload, + "expected_canonical": text, + "expected_canonical_hash": sha256_tagged(text), + }, "Keys MUST sort recursively; no whitespace; Unicode emitted raw, NOT " + "\\uXXXX-escaped. A serializer that escapes non-ASCII produces a different " + "hash and fails here.") + + write("valid.json", { + "envelope": envelope(RECEIPT, KEY_A, PUB_A), + "pinned_public_key": PUB_A, + "expect": {"verified": True, "hash_matches": True, "signature_valid": True, + "key_matches_pinned": True, "conforming": True}, + }, "The happy path. Verifies against the pinned key.") + + tampered = {**RECEIPT, "authority_level": 3} + env = envelope(RECEIPT, KEY_A, PUB_A) + env["receipt"] = tampered + write("tampered-payload.json", { + "envelope": env, + "pinned_public_key": PUB_A, + "expect": {"verified": False, "hash_matches": False, "signature_valid": False}, + }, "authority_level escalated 2 -> 3 after signing. MUST NOT verify.") + + write("resigned-other-key.json", { + "envelope": envelope(RECEIPT, KEY_B, PUB_B), + "pinned_public_key": PUB_A, + "expect": {"verified": False, "signature_valid": False, "key_matches_pinned": False}, + }, "Attacker minted their own keypair and signed a plausible receipt. Every hash " + "matches and the signature verifies against the EMBEDDED key. It MUST still " + "fail against the pinned key. An implementation that verifies against " + "envelope.public_key passes the other vectors and fails here.") + + write("no-canonical-hash.json", { + "envelope": envelope(RECEIPT, KEY_A, PUB_A, drop_hash=True), + "pinned_public_key": PUB_A, + "expect": {"verified": False, "hash_matches": False}, + }, "Signature is genuine but canonical_hash was stripped. An absent hash is NOT " + "a pass.") + + write("ephemeral-no-pinned-key.json", { + "envelope": envelope(RECEIPT, KEY_A, PUB_A, ephemeral=True), + "pinned_public_key": None, + "expect": {"verified": False, "refused": True}, + }, "key_ephemeral is true and the verifier was given no key to pin. Verifying " + "here proves nothing, so the verifier MUST refuse and say why rather than " + "return a green result.") + + write("auto-merge-true.json", { + "envelope": envelope({**RECEIPT, "auto_merge": True}, KEY_A, PUB_A), + "pinned_public_key": PUB_A, + "expect": {"signature_valid": True, "conforming": False}, + }, "Correctly signed, but auto_merge violates a signed invariant. The signature " + "is valid AND the receipt MUST be rejected as non-conforming. These are " + "separate checks.") + + (OUT / "keys.json").write_text(json.dumps({ + "_note": "TEST keys only. Seeds are published so vectors are reproducible. " + "Never use these to sign anything real.", + "key_A": {"seed": "sha256('signetry-conformance-vector-key-A')", "public_key": PUB_A}, + "key_B": {"seed": "sha256('signetry-conformance-vector-key-B')", "public_key": PUB_B}, + }, indent=2) + "\n") + print(" keys.json") + + +if __name__ == "__main__": + main() diff --git a/tests/conformance/test_receipt_conformance.py b/tests/conformance/test_receipt_conformance.py new file mode 100644 index 0000000..1ab78aa --- /dev/null +++ b/tests/conformance/test_receipt_conformance.py @@ -0,0 +1,203 @@ +"""Conformance suite for the Signetry Remediation Receipt format (RECEIPT_SPEC v1). + +This suite is deliberately written against the *specification*, not against the +implementation's internals. It reads static JSON vectors from ``vectors/`` and +asserts the outcomes the spec mandates. Two consequences follow, and both are the +point: + +1. **Any implementation can use it.** The vectors are language-agnostic JSON with + published test-key seeds. A Rust or Go verifier can be checked against exactly + these files. This directory and the spec are Apache-2.0 even though the engine + is not — see LICENSING.md. +2. **A passing signature is not a passing receipt.** Several vectors are correctly + signed and still MUST fail, because the format forbids what they claim. An + implementation that only checks cryptography fails here, which is the whole + reason the suite exists. + +Regenerate the vectors with ``python tests/conformance/generate_vectors.py``. +""" +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from signetry_core.pipeline.receipt import ( + _canonical, + _sha256, + check_invariants, + verify_receipt, +) + +VECTORS = Path(__file__).parent / "vectors" + + +def vector(name: str) -> dict: + return json.loads((VECTORS / f"{name}.json").read_text()) + + +# --- §5 Canonicalization ----------------------------------------------------- + + +def test_canonicalization_matches_the_published_vector(): + """The canonical form is byte-exact, or receipts do not survive a round trip. + + This vector carries non-ASCII text and deliberately out-of-order keys. The most + common way to fail it is a JSON serializer that escapes non-ASCII to \\uXXXX by + default (Python's ``json.dumps`` does) — that produces different bytes, a + different hash, and a signature nobody else can check. + """ + v = vector("canonicalization") + assert _canonical(v["payload"]) == v["expected_canonical"] + assert _sha256(v["expected_canonical"]) == v["expected_canonical_hash"] + + +def test_canonicalization_is_order_independent(): + """Two dicts with the same content in different insertion orders canonicalize + identically — otherwise the hash would depend on how the issuer happened to + build the object.""" + a = {"z": 1, "a": {"y": 2, "b": 3}} + b = {"a": {"b": 3, "y": 2}, "z": 1} + assert _canonical(a) == _canonical(b) + + +# --- §8 Verification --------------------------------------------------------- + + +def test_valid_receipt_verifies_against_the_pinned_key(): + v = vector("valid") + result = verify_receipt(v["envelope"], expected_public_key=v["pinned_public_key"]) + assert result["verified"] is True + assert result["signature_valid"] is True + assert result["hash_matches"] is True + assert result["key_matches_pinned"] is True + assert result["conforming"] is True + + +def test_tampered_payload_does_not_verify(): + """One field edited after signing (authority_level 2 -> 3). Both the hash and + the signature must catch it.""" + v = vector("tampered-payload") + result = verify_receipt(v["envelope"], expected_public_key=v["pinned_public_key"]) + assert result["verified"] is False + assert result["hash_matches"] is False + assert result["signature_valid"] is False + + +def test_receipt_signed_by_another_key_does_not_verify(capsys): + """§8.1, the load-bearing rule. + + This envelope is *internally perfect*: the hash matches, the signature verifies + against ``envelope['public_key']``, nothing is malformed. It was simply signed + by someone else's keypair. An implementation that verifies against the embedded + key passes every other test in this file and reports this forgery as valid. + """ + v = vector("resigned-other-key") + result = verify_receipt(v["envelope"], expected_public_key=v["pinned_public_key"]) + assert result["verified"] is False + assert result["signature_valid"] is False + assert result["key_matches_pinned"] is False + + # Prove the envelope really is self-consistent, so the failure above is + # attributable to key pinning and nothing else. + embedded = v["envelope"]["public_key"] + self_consistent = verify_receipt(v["envelope"], expected_public_key=embedded) + assert self_consistent["verified"] is True + + +def test_missing_canonical_hash_is_not_a_pass(): + """The signature here is genuine; the hash was stripped. Absence of evidence + must not read as evidence — no hash, not verified.""" + v = vector("no-canonical-hash") + result = verify_receipt(v["envelope"], expected_public_key=v["pinned_public_key"]) + assert result["hash_matches"] is False + assert result["verified"] is False + + +def test_ephemeral_key_with_no_pinned_key_is_refused(monkeypatch): + """§8.1 fail-closed. The dev seed is published in the source tree, so verifying + against it proves nothing. The verifier must refuse and say why rather than + return a green result a reader would misread as proof.""" + monkeypatch.delenv("SIGNETRY_SIGNING_KEY", raising=False) + v = vector("ephemeral-no-pinned-key") + result = verify_receipt(v["envelope"]) # no pinned key on purpose + assert result["verified"] is False + assert result["key_ephemeral"] is True + assert "refus" in result["reason"].lower() + + +# --- §4.3 Invariants --------------------------------------------------------- + + +def test_auto_merge_true_is_non_conforming_despite_a_valid_signature(): + """The separation the spec insists on: cryptography and conformance are + different questions. This receipt is signed correctly and must still be + rejected, because the format does not permit what it says.""" + v = vector("auto-merge-true") + result = verify_receipt(v["envelope"], expected_public_key=v["pinned_public_key"]) + assert result["signature_valid"] is True + assert result["hash_matches"] is True + assert result["conforming"] is False + assert any("auto_merge" in m for m in result["invariant_violations"]) + + +@pytest.mark.parametrize( + ("mutation", "expected_fragment"), + [ + ({"auto_merge": True}, "auto_merge"), + ({"human_review_required": False}, "human_review_required"), + ({"kind": "something.else"}, "kind"), + ({"version": 2}, "version"), + ({"authority_level": 4}, "authority_level"), + ({"authority_level": "2"}, "authority_level"), + ], +) +def test_each_invariant_is_enforced(mutation, expected_fragment): + receipt = dict(vector("valid")["envelope"]["receipt"]) + assert check_invariants(receipt) == [] # baseline is conforming + receipt.update(mutation) + violations = check_invariants(receipt) + assert any(expected_fragment in m for m in violations), violations + + +def test_issued_receipts_are_conforming_by_construction(): + """Whatever the pipeline is asked to do, it cannot emit a receipt that claims + self-merge authority — the invariant is set at construction, inside the signed + payload.""" + from signetry_core.pipeline.receipt import build_receipt + + envelope = build_receipt( + repo="Signetry/conformance", + base_commit="0" * 40, + contract={}, + contract_result={"ok": True}, + verifier=None, + trust_boundary=None, + proposed_change=None, + providers=None, + authority_level=0, + authority="analyze", + ) + assert check_invariants(envelope["receipt"]) == [] + assert envelope["receipt"]["auto_merge"] is False + assert envelope["receipt"]["human_review_required"] is True + + +# --- Vector hygiene ---------------------------------------------------------- + + +def test_every_vector_is_exercised(): + """A vector nobody asserts on is decoration. If you add a file to vectors/, + add a test and list it here.""" + covered = { + "canonicalization", "valid", "tampered-payload", "resigned-other-key", + "no-canonical-hash", "ephemeral-no-pinned-key", "auto-merge-true", + } + present = {p.stem for p in VECTORS.glob("*.json")} - {"keys"} + assert present == covered, f"unexercised or missing vectors: {present ^ covered}" + + +def test_every_vector_documents_what_it_proves(): + for path in VECTORS.glob("*.json"): + assert json.loads(path.read_text()).get("_note"), f"{path.name} has no _note" diff --git a/tests/conformance/vectors/auto-merge-true.json b/tests/conformance/vectors/auto-merge-true.json new file mode 100644 index 0000000..de39b43 --- /dev/null +++ b/tests/conformance/vectors/auto-merge-true.json @@ -0,0 +1,30 @@ +{ + "envelope": { + "receipt": { + "kind": "signetry.remediation-receipt", + "version": 1, + "generated_at": "2026-08-31T12:00:00+00:00", + "repo": "Signetry/conformance", + "base_commit": "0000000000000000000000000000000000000000", + "executor": "none", + "policy_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "diff_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authority_level": 2, + "authority": "branch_pr_only", + "outcome": "Conformance fixture — not a real remediation.", + "auto_merge": true, + "human_review_required": true + }, + "canonical_hash": "sha256:3ee3cd29dd53ad9af6075312b75f7e90a547702783ca39f946895ed080dbfadb", + "signature": "6bf0DeSUy2bPgaIjERsppmXDYmJYDEnrjToNRY337mBRj+l7Uukq5yk6DLERAcP1tba/hFW/M9hh5FHWwgAwDQ==", + "public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=", + "algorithm": "Ed25519", + "key_ephemeral": false + }, + "pinned_public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=", + "expect": { + "signature_valid": true, + "conforming": false + }, + "_note": "Correctly signed, but auto_merge violates a signed invariant. The signature is valid AND the receipt MUST be rejected as non-conforming. These are separate checks." +} diff --git a/tests/conformance/vectors/canonicalization.json b/tests/conformance/vectors/canonicalization.json new file mode 100644 index 0000000..fee37a7 --- /dev/null +++ b/tests/conformance/vectors/canonicalization.json @@ -0,0 +1,19 @@ +{ + "payload": { + "zeta": 1, + "alpha": { + "nested_z": true, + "nested_a": [ + 3, + 2, + 1 + ] + }, + "unicode": "receipt — Ünïcödé ✓ 日本語", + "empty": {}, + "null": null + }, + "expected_canonical": "{\"alpha\":{\"nested_a\":[3,2,1],\"nested_z\":true},\"empty\":{},\"null\":null,\"unicode\":\"receipt — Ünïcödé ✓ 日本語\",\"zeta\":1}", + "expected_canonical_hash": "sha256:a96172215b2239b0362293654fa95020598663e58fd665172dcb251654ce6366", + "_note": "Keys MUST sort recursively; no whitespace; Unicode emitted raw, NOT \\uXXXX-escaped. A serializer that escapes non-ASCII produces a different hash and fails here." +} diff --git a/tests/conformance/vectors/ephemeral-no-pinned-key.json b/tests/conformance/vectors/ephemeral-no-pinned-key.json new file mode 100644 index 0000000..c1820ce --- /dev/null +++ b/tests/conformance/vectors/ephemeral-no-pinned-key.json @@ -0,0 +1,30 @@ +{ + "envelope": { + "receipt": { + "kind": "signetry.remediation-receipt", + "version": 1, + "generated_at": "2026-08-31T12:00:00+00:00", + "repo": "Signetry/conformance", + "base_commit": "0000000000000000000000000000000000000000", + "executor": "none", + "policy_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "diff_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authority_level": 2, + "authority": "branch_pr_only", + "outcome": "Conformance fixture — not a real remediation.", + "auto_merge": false, + "human_review_required": true + }, + "canonical_hash": "sha256:a550860f9e5e302bb03d6190cf532a17b95b5ee6c5e100a773fcf8d7afa7032e", + "signature": "91lq9d2ya2R91DrBN/XPb7WCJM3xXSRQ/gU3GFbUfJWSItZtw4zCNXq88vMrEJJpMNsibCbk+5Ykpf335LrFBw==", + "public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=", + "algorithm": "Ed25519", + "key_ephemeral": true + }, + "pinned_public_key": null, + "expect": { + "verified": false, + "refused": true + }, + "_note": "key_ephemeral is true and the verifier was given no key to pin. Verifying here proves nothing, so the verifier MUST refuse and say why rather than return a green result." +} diff --git a/tests/conformance/vectors/keys.json b/tests/conformance/vectors/keys.json new file mode 100644 index 0000000..89a2299 --- /dev/null +++ b/tests/conformance/vectors/keys.json @@ -0,0 +1,11 @@ +{ + "_note": "TEST keys only. Seeds are published so vectors are reproducible. Never use these to sign anything real.", + "key_A": { + "seed": "sha256('signetry-conformance-vector-key-A')", + "public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=" + }, + "key_B": { + "seed": "sha256('signetry-conformance-vector-key-B')", + "public_key": "vuNB+DEcLlLA1czWNk+va8wOv15poeq5NVsvXcVMKfw=" + } +} diff --git a/tests/conformance/vectors/no-canonical-hash.json b/tests/conformance/vectors/no-canonical-hash.json new file mode 100644 index 0000000..c135edf --- /dev/null +++ b/tests/conformance/vectors/no-canonical-hash.json @@ -0,0 +1,29 @@ +{ + "envelope": { + "receipt": { + "kind": "signetry.remediation-receipt", + "version": 1, + "generated_at": "2026-08-31T12:00:00+00:00", + "repo": "Signetry/conformance", + "base_commit": "0000000000000000000000000000000000000000", + "executor": "none", + "policy_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "diff_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authority_level": 2, + "authority": "branch_pr_only", + "outcome": "Conformance fixture — not a real remediation.", + "auto_merge": false, + "human_review_required": true + }, + "signature": "91lq9d2ya2R91DrBN/XPb7WCJM3xXSRQ/gU3GFbUfJWSItZtw4zCNXq88vMrEJJpMNsibCbk+5Ykpf335LrFBw==", + "public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=", + "algorithm": "Ed25519", + "key_ephemeral": false + }, + "pinned_public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=", + "expect": { + "verified": false, + "hash_matches": false + }, + "_note": "Signature is genuine but canonical_hash was stripped. An absent hash is NOT a pass." +} diff --git a/tests/conformance/vectors/resigned-other-key.json b/tests/conformance/vectors/resigned-other-key.json new file mode 100644 index 0000000..7f051c7 --- /dev/null +++ b/tests/conformance/vectors/resigned-other-key.json @@ -0,0 +1,31 @@ +{ + "envelope": { + "receipt": { + "kind": "signetry.remediation-receipt", + "version": 1, + "generated_at": "2026-08-31T12:00:00+00:00", + "repo": "Signetry/conformance", + "base_commit": "0000000000000000000000000000000000000000", + "executor": "none", + "policy_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "diff_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authority_level": 2, + "authority": "branch_pr_only", + "outcome": "Conformance fixture — not a real remediation.", + "auto_merge": false, + "human_review_required": true + }, + "canonical_hash": "sha256:a550860f9e5e302bb03d6190cf532a17b95b5ee6c5e100a773fcf8d7afa7032e", + "signature": "Pgym7PtVvvIk+mgjuvpl0i6PuIkfgtScMSTBwKo3wsNUOMRxP1n4heBAIjW45oLcMD26FchuX7h9czHc6PUQBQ==", + "public_key": "vuNB+DEcLlLA1czWNk+va8wOv15poeq5NVsvXcVMKfw=", + "algorithm": "Ed25519", + "key_ephemeral": false + }, + "pinned_public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=", + "expect": { + "verified": false, + "signature_valid": false, + "key_matches_pinned": false + }, + "_note": "Attacker minted their own keypair and signed a plausible receipt. Every hash matches and the signature verifies against the EMBEDDED key. It MUST still fail against the pinned key. An implementation that verifies against envelope.public_key passes the other vectors and fails here." +} diff --git a/tests/conformance/vectors/tampered-payload.json b/tests/conformance/vectors/tampered-payload.json new file mode 100644 index 0000000..0a6d198 --- /dev/null +++ b/tests/conformance/vectors/tampered-payload.json @@ -0,0 +1,31 @@ +{ + "envelope": { + "receipt": { + "kind": "signetry.remediation-receipt", + "version": 1, + "generated_at": "2026-08-31T12:00:00+00:00", + "repo": "Signetry/conformance", + "base_commit": "0000000000000000000000000000000000000000", + "executor": "none", + "policy_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "diff_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authority_level": 3, + "authority": "branch_pr_only", + "outcome": "Conformance fixture — not a real remediation.", + "auto_merge": false, + "human_review_required": true + }, + "canonical_hash": "sha256:a550860f9e5e302bb03d6190cf532a17b95b5ee6c5e100a773fcf8d7afa7032e", + "signature": "91lq9d2ya2R91DrBN/XPb7WCJM3xXSRQ/gU3GFbUfJWSItZtw4zCNXq88vMrEJJpMNsibCbk+5Ykpf335LrFBw==", + "public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=", + "algorithm": "Ed25519", + "key_ephemeral": false + }, + "pinned_public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=", + "expect": { + "verified": false, + "hash_matches": false, + "signature_valid": false + }, + "_note": "authority_level escalated 2 -> 3 after signing. MUST NOT verify." +} diff --git a/tests/conformance/vectors/valid.json b/tests/conformance/vectors/valid.json new file mode 100644 index 0000000..7a1bf9f --- /dev/null +++ b/tests/conformance/vectors/valid.json @@ -0,0 +1,33 @@ +{ + "envelope": { + "receipt": { + "kind": "signetry.remediation-receipt", + "version": 1, + "generated_at": "2026-08-31T12:00:00+00:00", + "repo": "Signetry/conformance", + "base_commit": "0000000000000000000000000000000000000000", + "executor": "none", + "policy_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "diff_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authority_level": 2, + "authority": "branch_pr_only", + "outcome": "Conformance fixture — not a real remediation.", + "auto_merge": false, + "human_review_required": true + }, + "canonical_hash": "sha256:a550860f9e5e302bb03d6190cf532a17b95b5ee6c5e100a773fcf8d7afa7032e", + "signature": "91lq9d2ya2R91DrBN/XPb7WCJM3xXSRQ/gU3GFbUfJWSItZtw4zCNXq88vMrEJJpMNsibCbk+5Ykpf335LrFBw==", + "public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=", + "algorithm": "Ed25519", + "key_ephemeral": false + }, + "pinned_public_key": "6bJlbnL62YP77IuopgrIJp0ChlrElf9zpLpI05mh4IE=", + "expect": { + "verified": true, + "hash_matches": true, + "signature_valid": true, + "key_matches_pinned": true, + "conforming": true + }, + "_note": "The happy path. Verifies against the pinned key." +} From 40a1672be07e26cdf3ed236c5c1ab52ae7716cc1 Mon Sep 17 00:00:00 2001 From: Binay <150876063+bkd-dotcom@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:35:40 -0400 Subject: [PATCH 2/2] =?UTF-8?q?spec:=20=C2=A79=20=E2=80=94=20placeholder?= =?UTF-8?q?=20provenance=20MUST=20be=20treated=20as=20absent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §9 said provenance fields are OPTIONAL and "when absent the policy MUST be treated as unsigned". `signetry init` writes `policy_owner: your-team`, which is *present* — so the letter of the spec was satisfied by exactly the value that means nobody has adopted the policy. A verifier testing for a non-empty string would report a policy no human has read as change-controlled. The addition closes that: presence is not a declaration. Scaffold placeholder text MUST be treated as if the field were absent, because converting an unanswered question into a false answer is strictly worse than reporting it unowned. Consumers MAY distinguish placeholder from absent when explaining *why* a policy is unowned, but neither may confer provenance. No `version` bump: per §12 that is reserved for changes which would make a conforming verifier reject a valid receipt, and this changes how a value is interpreted and reported, not whether a receipt is accepted. The spec is being published for the first time here, so this is part of v1 as published rather than an amendment to it. The reference implementation of this requirement is core#108, which stops `policy_status()` returning `declared` for scaffold text. Until that lands, this engine does not yet satisfy its own §9 — merge the two adjacently. --- docs/RECEIPT_SPEC.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/RECEIPT_SPEC.md b/docs/RECEIPT_SPEC.md index 2f5a11f..c6ff3b3 100644 --- a/docs/RECEIPT_SPEC.md +++ b/docs/RECEIPT_SPEC.md @@ -237,6 +237,14 @@ class" and MUST NOT be interpreted as a widening. Provenance fields (`policy_owner`, `policy_version`, `policy_approved_at`) are OPTIONAL; when absent the policy MUST be treated as unsigned and surfaced as such, never silently trusted. +A provenance value that is **scaffold placeholder text** — `your-team`, `TODO`, +`changeme`, a starter-template or registry default — MUST be treated exactly as if the +field were absent. Presence is not a declaration. A verifier that tests only for a +non-empty string will report a policy nobody has read as change-controlled, which is +strictly worse than reporting it as unowned, because it converts an unanswered question +into a false answer. Consumers MAY distinguish placeholder from absent when surfacing +*why* a policy is unowned, but MUST NOT let either one confer provenance. + ## 10. Authority ladder | Level | Meaning |