Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 50 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,34 @@ permissions:

jobs:
python:
name: Python verifier + vectors
name: Python ${{ matrix.python }} verifier + lifecycle policy
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.8", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install cryptography
python-version: ${{ matrix.python }}
- run: python -m pip install -e clients/python pytest build
- name: lifecycle and anchor regressions
run: python -m pytest clients/python/tests
- name: non-actionable verified-agent regressions
run: python -m unittest examples/verified-agent/test_agent.py
- name: conformance vectors
run: python3 tests/verify_vectors.py
- name: package build
run: python -m build clients/python
- name: clean-wheel lifecycle and encoding regressions
run: |
clean_venv="$RUNNER_TEMP/dynamicfeed-verify-wheel"
python -m venv "$clean_venv"
"$clean_venv/bin/pip" install pytest clients/python/dist/*.whl
cd "$RUNNER_TEMP"
"$clean_venv/bin/python" -m pytest "$GITHUB_WORKSPACE/clients/python/tests"
- name: notebook JSON integrity
run: python -m json.tool examples/openai/grounding_on_verifiable_live_data.ipynb >/dev/null
- name: reliability self-test
run: python3 reliability/verify_reliability.py --selftest

Expand All @@ -32,8 +50,19 @@ jobs:
with:
node-version: "20"
- run: npm install --prefix clients/js
- name: lifecycle and anchor regressions
run: npm test --prefix clients/js
- name: conformance vectors
run: node tests/verify_vectors.mjs
- name: package and test clean tarball
run: |
package_dir="$RUNNER_TEMP/dynamicfeed-verify-package"
consumer_dir="$RUNNER_TEMP/dynamicfeed-verify-consumer"
mkdir -p "$package_dir" "$consumer_dir"
(cd clients/js && npm pack --pack-destination "$package_dir")
npm install --prefix "$consumer_dir" "$package_dir"/*.tgz
DF_VERIFY_PACKAGE_ENTRY="$consumer_dir/node_modules/@dynamicfeed/verify/index.js" \
node clients/js/tests/anchor.test.js
- name: reliability self-test
run: node reliability/verify_reliability.js --selftest

Expand All @@ -49,5 +78,21 @@ jobs:
with:
components: rustfmt, clippy
- run: cargo fmt --check
- run: cargo clippy --all-targets -- -D warnings
- run: cargo test
- run: cargo clippy --all-targets --all-features -- -D warnings
- run: cargo test --all-features
- name: package and test packaged source
run: |
cargo package --allow-dirty
package_dir="$(mktemp -d)"
tar -xzf target/package/dynamicfeed-verify-1.0.2.crate -C "$package_dir"
cargo test --all-features --manifest-path "$package_dir/dynamicfeed-verify-1.0.2/Cargo.toml"

rotation-policy:
name: Signing-key rotation policy guardrail
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python scripts/check_rotation_policy.py
18 changes: 13 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
# Contributing

Thanks for your interest. The goal of this repo is one thing done well: **independently verifying DF-VERIFY/1 signed responses, byte-for-byte, in any language**, plus the reliability object that grades how much to believe a datapoint.
Thanks for your interest. The goal of this repository is one thing done well: independently check
DF-VERIFY/1 signature bytes and apply caller-authenticated signer-lifecycle policy consistently
across languages, while keeping the separate reliability object honest.

## The bar

Every verifier, in every language, must agree. That agreement is defined by the language-agnostic vectors in [`tests/vectors`](tests/vectors), not by any one implementation:
Every supported reference verifier must agree on signature mathematics and lifecycle policy. The C#
sample is quarantined until it reaches that bar. Agreement is defined by the language-agnostic
vectors and lifecycle regressions, not by any one implementation:

- **Canonicalization** must match the expected bytes exactly (keys sorted recursively, compact separators, non-ASCII escaped `\uXXXX` including astral surrogate pairs, numbers verbatim, top-level `signature` stripped).
- The **authentic** signed envelope must verify; the **tampered** twin must be rejected.
- Authentic historical bytes may be cryptographically valid while lifecycle-rejected. Tampered
signed bytes must be cryptographically rejected. Compromised signers must never pass live policy.
- Registry revision floors, explicit historical mode, and active-only live acceptance must match.
- For the reliability object, the cases in [`reliability/conformance-vectors.json`](reliability/conformance-vectors.json) must pass (the honesty rules, including `signed != verified`).

CI runs all of this on every push and pull request. A change is mergeable when CI is green.

## Adding or porting a verifier

1. Put it under `clients/<language>/`.
2. Reproduce the canonicalization and the Ed25519 check; do not invent a new format.
2. Reproduce strict canonicalization, Ed25519, key-ID binding, lifecycle validation, and caller-held
revision floors; do not invent a new format.
3. Add a test (or harness) that runs the vectors in `tests/vectors`, and wire it into [`.github/workflows/ci.yml`](.github/workflows/ci.yml).
4. Open a PR. Keep it dependency-light and offline-capable: a verifier should need nothing from the issuer at runtime beyond fetching the public key.
4. Open a PR. Keep it dependency-light and offline-capable. A flat public key is crypto-only;
policy-aware offline use requires a reviewed lifecycle registry.

## Running the checks locally

Expand Down
28 changes: 25 additions & 3 deletions KNOWN_KEYS.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
{
"note": "Dynamic Feed Ed25519 signing keys, pinned OUTSIDE dynamicfeed.ai so receipts remain verifiable even if the domain ceases to exist (Principle 1, the courtroom test). key_id = 'df-ed25519-' + sha256(raw_public_key)[:12]. Keys are only ever ADDED here, never removed. Live set: https://dynamicfeed.ai/.well-known/keys",
"schema": "df-known-key-archive/v2",
"deprecated": true,
"superseded_by": "SIGNING_KEY_LIFECYCLE.json",
"note": "Compatibility archive of public key bytes only. This file is deliberately not a flat key map and MUST NOT be used to make a signer-acceptance decision. Validate SIGNING_KEY_LIFECYCLE.json and apply lifecycle policy instead.",
"keys": {
"df-ed25519-4cb32e72f333": "O4Kw2r-BjuDRL_Uyj3Vs8i-SnqHZUtPfARfj27NKEfk="
"df-ed25519-4cb32e72f333": {
"public_key_base64url": "O4Kw2r-BjuDRL_Uyj3Vs8i-SnqHZUtPfARfj27NKEfk=",
"status": "compromised",
"compromised_after": "2026-07-11T00:00:00Z",
"policy_accepted": false
},
"df-ed25519-6ca0de29113b": {
"public_key_base64url": "acMWky59eQfoVqCUgmUM3tcl35YioRngL4wVDIQsstg=",
"status": "active",
"active_from": "2026-07-11T09:49:41Z",
"policy_accepted": false,
"reason": "This deprecated compatibility archive is not lifecycle authority; validate SIGNING_KEY_LIFECYCLE.json."
}
},
"first_published": "2026-07-05"
"policy": {
"flat_key_acceptance_forbidden": true,
"cryptographic_validity_separate_from_lifecycle": true,
"compromised_keys_never_policy_accepted": true,
"historical_public_key_bytes_retained": true
},
"first_published": "2026-07-05",
"last_reviewed": "2026-07-11"
}
201 changes: 149 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,79 +1,176 @@
# DF-VERIFY/1: verifiable grounding for AI that acts
# DF-VERIFY/1 verifier sources

Reference verifier source for Dynamic Feed signed envelopes and signing-key lifecycle policy.
The security boundary is deliberately split:

- `crypto_valid` means the Ed25519 signature, exact metadata, canonical bytes, and key-ID binding
passed;
- `ok` / policy acceptance additionally requires an acceptable signer in a validated lifecycle
registry whose source the verifier authenticated;
- a signature proves integrity under that key, not objective truth, safety, or legal compliance.

## Security notice — 2026-07-11

Earlier published verifiers accepted a flat public-key map and had no retired/compromised signer
policy. The former signing key `df-ed25519-4cb32e72f333` is now classified **compromised** using the
conservative boundary `2026-07-11T00:00:00Z`. Its public bytes are retained so historical signature
mathematics remain inspectable, but that key must never produce a live policy-accepted result.

The hardened versions in this repository are release targets, not evidence that package registries
have already been updated:

| Ecosystem | Affected published version(s) | Hardened source target | Published by this PR? |
|---|---:|---:|---|
| PyPI `dynamicfeed-verify` | `<= 1.0.2` | `1.0.3` | No |
| npm `@dynamicfeed/verify` | `<= 1.0.1` | `1.0.2` | No |
| crates.io `dynamicfeed-verify` | `1.0.0` (and affected prior source `1.0.1`) | `1.0.2` | No |
| C# sample | all current source | quarantined, transport-only | Not a package |

Do not announce a clean package rotation until the target artifacts are built from the reviewed
commit, published through their normal release controls, installed from each registry into a clean
environment, and the advisory is updated with artifact digests and provenance.

See [SECURITY.md](SECURITY.md) for the full advisory.

## Trust material

- [`SIGNING_KEY_LIFECYCLE.json`](SIGNING_KEY_LIFECYCLE.json) is the reviewed
`df-signing-key-registry/v1` snapshot. It retains active and historical public bytes with explicit
lifecycle status and `registry_revision`.
- [`KNOWN_KEYS.json`](KNOWN_KEYS.json) is a deprecated compatibility archive. It is intentionally
not a flat map and cannot honestly authorize the compromised key.
- The service compatibility endpoint `/.well-known/keys` is required to become active-key-only as
part of the corresponding service deployment. Historical key bytes belong in the lifecycle
registry. Verify the live endpoint after deployment; this source PR does not deploy it.

The current-domain registry is an operational disclosure, not an independent trust root. A
security-sensitive verifier should pin a reviewed registry out of band, authenticate updates, and
persist the highest authenticated `registry_revision` outside the document.

## What policy-aware verification does

1. Strictly parse JSON and reject duplicate object names, malformed numbers, raw controls, and
trailing bytes.
2. Require exactly `Ed25519` and `json-sorted-compact` signature metadata.
3. Preserve both frozen receipt conventions: verify without `signature` first, then (only when
needed) without both `signature` and a legacy post-signature `anchor`.
4. Derive `key_id` from the SHA-256 fingerprint of the raw public key.
5. Validate the lifecycle registry, its key/fingerprint relationships, chronology, policy fields,
active-key uniqueness, and revision floor.
6. Accept only the current active key in live mode. Retired keys are non-live historical material;
compromised keys are never policy accepted.
7. Keep historical snapshot inspection explicit and non-actionable: it can report signature
mathematics and policy-as-of state but never returns an overall live acceptance.

`anchor_authenticated=true` means an attached anchor was inside the signed bytes.
`anchor_authenticated=false` means the frozen legacy post-signature attachment was outside them.
Neither state independently validates an RFC 3161 or OpenTimestamps proof; these libraries do not
claim full timestamp-proof verification.

## Repository map

| Path | Status |
|---|---|
| [`clients/python`](clients/python) | Python lifecycle-aware verifier, target `1.0.3` |
| [`clients/js`](clients/js) | JavaScript/TypeScript lifecycle-aware verifier, target `1.0.2` |
| [`clients/rust`](clients/rust) | Rust crate `dynamicfeed-verify`, lifecycle-aware target `1.0.2` |
| [`clients/csharp`](clients/csharp) | Quarantined transport-only sample; no reference-verifier claim |
| [`examples/verified-agent`](examples/verified-agent) | Verify-before-use example; never an actuator |
| [`tests/vectors`](tests/vectors) | Shared canonicalization and frozen receipt fixtures |
| [`reliability`](reliability) | Separate non-cryptographic reliability vocabulary and validators |

Reference implementation of **[DF-VERIFY/1](https://dynamicfeed.ai/standard)**, an open, vendor-neutral standard for cryptographically signing, publishing, and **independently verifying exactly what an AI system was told the moment it acted**.
### Rust package provenance

When an AI *acts* (it moves a robot, places a trade, files a claim), "trust me" is not an audit trail. DF-VERIFY attaches an Ed25519 signature to any JSON response, publishes the verifying key openly, and lets anyone check it with **no account and no dependency on the issuer**. You can verify, even against the issuer.
This repository's [`clients/rust`](clients/rust) is the source for the crate named
`dynamicfeed-verify`. The distinct monorepo crate named `df-verify` (`0.1.2` target) is maintained at
`Dynamic-Feed/packages/df-verify-rs`; it is not source-identical or package-identical. Its Cargo
`repository` metadata must point to its actual source location (or that exact source must be added
here under an unambiguous path) before publication.

[![conformance](https://github.com/dynamicfeed/df-verify/actions/workflows/ci.yml/badge.svg)](https://github.com/dynamicfeed/df-verify/actions/workflows/ci.yml)
[![DF-VERIFY/1](https://dynamicfeed.ai/badge.svg)](https://dynamicfeed.ai/standard)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
## Verify from source

Every verifier here (Python, JavaScript, C#, Rust) is held to the **same language-agnostic conformance vectors**, run in CI on every commit. Canonicalization must match byte-for-byte, the authentic signature must verify, and the tampered twin must be rejected.
Python:

## What's here
```bash
python -m pip install -e clients/python pytest
python -m pytest clients/python/tests
python tests/verify_vectors.py
```

| Path | What |
|---|---|
| [`clients/python`](clients/python) | `dynamicfeed-verify`: Python reference verifier (library + CLI) |
| [`clients/js`](clients/js) | `@dynamicfeed/verify`: JavaScript/TypeScript verifier (Node, Deno, Bun, browser) |
| [`clients/csharp`](clients/csharp) | C# reference verifier |
| [`clients/rust`](clients/rust) | `dynamicfeed-verify`: Rust reference verifier (library + CLI), passes the shared vectors |
| [`examples/verified-agent`](examples/verified-agent) | a runnable agent that verifies a signature **before it acts** |
| [`tests/vectors`](tests/vectors) | language-agnostic conformance vectors + Python & JS harnesses |
JavaScript:

## Verify in 30 seconds
```bash
npm install --prefix clients/js
npm test --prefix clients/js
node tests/verify_vectors.mjs
```

Both reference verifiers are published. Install one and check a live signed verdict in two lines.
Rust:

**Python** (`pip install dynamicfeed-verify`)
```python
from dynamicfeed_verify import verify_live
env, result = verify_live() # fetch a fresh signed verdict and verify it
assert result["ok"] # tampered or unsigned: this fails
```bash
cd clients/rust
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
```

**JavaScript** (`npm i @dynamicfeed/verify`)
```js
import { verifyLive } from '@dynamicfeed/verify';
const { result } = await verifyLive();
if (!result.ok) throw new Error(`unverified world-state: ${result.error}`);
```
Rotation guardrail:

**Or run the demo agent from source** (verify-before-act, and watch it refuse when tampered):
```bash
git clone https://github.com/dynamicfeed/df-verify && cd df-verify
pip install cryptography
python examples/verified-agent/agent.py # verify a live verdict, then act
python examples/verified-agent/agent.py --tamper # altered after signing, the agent refuses to act
python scripts/check_rotation_policy.py
```

## How it works
## Minimal policy-aware use

1. Drop the `signature` block; keep the rest as the payload.
2. Canonicalize: JSON, keys sorted recursively, compact separators, non-ASCII escaped `\uXXXX`, UTF-8.
3. Fetch the public key from `https://dynamicfeed.ai/.well-known/keys`, look up `signature.key_id`.
4. Verify the Ed25519 signature over the canonical bytes. Change one byte → it fails.
Python:

## Conformance
```python
import json
from dynamicfeed_verify import verify

registry = json.load(open("SIGNING_KEY_LIFECYCLE.json"))
result = verify(
envelope,
lifecycle_registry=registry,
registry_source_authenticated=True, # caller authenticated this pinned file
)
if not result["ok"]:
raise RuntimeError(result["error"])
```

```bash
python3 tests/verify_vectors.py # Python harness → "✓ ALL 10 VECTORS PASS"
node tests/verify_vectors.mjs # JS harness (run: npm install --prefix clients/js first)
JavaScript:

```js
import { verify } from '@dynamicfeed/verify';

const result = await verify(rawEnvelopeText, {
lifecycleRegistry: pinnedRegistry,
registrySourceAuthenticated: true,
});
if (!result.ok) throw new Error(result.error);
```

Both reference verifiers reproduce every vector (same canonical bytes, same signature verdicts), so you can confirm a new implementation in any language byte-for-byte.
Rust:

```rust
let registry = dynamicfeed_verify::LifecycleRegistry::parse_with_options(
&registry_text,
dynamicfeed_verify::RegistryValidationOptions {
registry_source_authenticated: true,
..Default::default()
},
)?;
let result = dynamicfeed_verify::verify_with_registry(&raw_envelope, &registry)?;
assert!(result.accepted);
```

## Links

- **Spec:** https://dynamicfeed.ai/standard
- **Verify in your browser:** https://dynamicfeed.ai/proof
- **Public keys (a key_id to Ed25519 public-key map):** https://dynamicfeed.ai/.well-known/keys
- **Discovery manifest:** https://dynamicfeed.ai/.well-known/df-verify.json
- Specification surface: https://dynamicfeed.ai/standard
- Browser verifier: https://dynamicfeed.ai/proof
- Lifecycle registry endpoint: https://dynamicfeed.ai/.well-known/signing-key-registry.json
- Active-key compatibility endpoint: https://dynamicfeed.ai/.well-known/keys

## License

MIT.

## Reliability axis

Beyond signing *what* was said, the [`reliability/`](reliability) toolkit grades *how much to believe it*: the OKF reliability object (schema + zero-dep Python & JS validators), enforcing `signed != verified`. See [reliability/README.md](reliability/README.md).
Loading
Loading