Skip to content

Commit 3b2bfee

Browse files
committed
conformance: add Signet self-certification template
Pre-fills a conformance mapping for Signet (Prismer-AI/signet) as a template the maintainer can confirm, correct, or extend. Avoids blocking conformance on a cold start from zero. Populated from the public Signet source at crates/signet-core/src/{receipt.rs, canonical.rs, sign.rs}: - FIELD-MAPPING.md: Signet -> draft-02 field-by-field mapping with [CONFIRM] / [FILL] markers on each inferred entry. - DEVIATIONS.md: four documented wire-format deltas (envelope shape, signature prefix, chain linkage by ID vs hash, key identifier format) with adapter guidance for each. - ADAPTER.md: pseudocode sketch of a thin Signet -> draft-02 shim in co-signer mode, plus open questions for the maintainer. - README.md: what is here and what the maintainer needs to do to turn this template into a conformance claim. Updates IMPLEMENTATIONS.md: Signet row moves from "Under review" to "Self-certification template staged" with a pointer to the template directory. Context: microsoft/agent-governance-toolkit#1201 (Tutorial 33 cross- implementation table addition) triggered the conformance conversation with @willamhou. The template is an artifact that lowers Signet-side activation energy from "design a mapping from scratch" to "confirm or correct a pre-filled mapping."
1 parent 0fa18bf commit 3b2bfee

5 files changed

Lines changed: 302 additions & 1 deletion

File tree

IMPLEMENTATIONS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
| protect-mcp | [scopeblind/scopeblind-gateway](https://github.com/scopeblind/scopeblind-gateway) | Claimed | @tomjwxf | Claude Code hooks + MCP gateway. Operator-signed mode. |
77
| protect-mcp-adk | [scopeblind/protect-mcp-adk](https://github.com/scopeblind/protect-mcp-adk) | Claimed | @tomjwxf | Google ADK BasePlugin. Python. |
88
| aps-governance-hook | [aeoess/hermes-aps-delegation](https://github.com/aeoess/hermes-aps-delegation) (pending 0.1.0) | Claimed | @aeoess | Authority-chain-referenced mode with delegation_chain_root. |
9-
| Signet | TBD | Under review | @willamhou | Added to AGT Tutorial 33 via [microsoft/agent-governance-toolkit#1201](https://github.com/microsoft/agent-governance-toolkit/pull/1201); conformance under confirmation. |
9+
| Signet | [Prismer-AI/signet](https://github.com/Prismer-AI/signet) | Self-certification template staged | @willamhou | Conformance template pre-filled at [conformance/signet/](./conformance/signet/) for maintainer sign-off. Wire format shares JCS + Ed25519 with draft-02; four documented deltas (envelope shape, signature prefix, chain linkage by ID vs hash, key identifier) are adapter-addressable. Context: [microsoft/agent-governance-toolkit#1201](https://github.com/microsoft/agent-governance-toolkit/pull/1201). |
1010

1111
## How to add an implementation
1212

conformance/signet/ADAPTER.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Signet -> draft-02 adapter sketch
2+
3+
Pseudocode for a thin shim that translates a Signet receipt (as emitted by `Prismer-AI/signet@main` at the date of this PR) into a draft-farley-acta-signed-receipts-02 envelope verifiable against `@veritasacta/verify`.
4+
5+
**Status:** Sketch only. Production implementation is out of scope for this template. Provided so the Signet maintainer can evaluate whether the adapter path is thin enough to pursue, or whether native alignment (Path 3 on [microsoft/agent-governance-toolkit#1201](https://github.com/microsoft/agent-governance-toolkit/pull/1201)) is the better move.
6+
7+
## Input
8+
9+
A signed Signet receipt, as emitted by `signet_core::sign::sign()`.
10+
11+
## Output
12+
13+
Either:
14+
15+
- **A draft-02 envelope that verifies against `@veritasacta/verify --key operator.pem`**, IF the adapter has access to the Signet operator's pubkey and can publish a JWKS at the correct discovery endpoint; OR
16+
- **A draft-02 envelope that fails verification with a deterministic `invalid_signature` error** — because the signature was generated over Signet's canonical form, not draft-02's. To produce a verifying draft-02 envelope, the adapter must re-sign.
17+
18+
The latter is usually what is wanted: the adapter acts as a **co-signer** at the governance gateway, producing a draft-02 envelope in parallel with the Signet receipt. Both receipts are emitted; downstream consumers choose which to verify.
19+
20+
## Pseudocode (co-signer mode)
21+
22+
```python
23+
def signet_to_draft02(signet_receipt: dict, operator_signer: Ed25519Signer) -> dict:
24+
"""Co-sign a Signet receipt as a draft-02 envelope.
25+
26+
signet_receipt: the decoded JSON of a Signet receipt.
27+
operator_signer: the same Ed25519 key that signed the Signet receipt,
28+
wrapped for draft-02-style JCS canonicalization.
29+
30+
Returns: a draft-02 envelope.
31+
"""
32+
action = signet_receipt["action"]
33+
34+
payload = {
35+
"type": "signet:decision",
36+
"action": action["tool"],
37+
"agent_id": signet_receipt["signer"].get("name", "unknown"),
38+
"issuer_id": signet_receipt["signer"].get("owner", "unknown"),
39+
"issued_at": signet_receipt["ts"],
40+
"decision": "allow", # Or pull from signet's decision field if present
41+
}
42+
43+
# Optional mappings (only if present in Signet)
44+
if "params_hash" in action:
45+
payload["action_ref"] = action["params_hash"]
46+
if "trace_id" in action:
47+
payload["iteration_id"] = action["trace_id"]
48+
if "parent_receipt_id" in action:
49+
# Requires the prior receipt (or its hash) to be resolvable.
50+
# See DEVIATIONS.md section 3.
51+
payload["previousReceiptHash"] = resolve_prior_hash(action["parent_receipt_id"])
52+
53+
# Optional: map policy_attestation -> policy_digest/policy_id
54+
if signet_receipt.get("policy"):
55+
policy = signet_receipt["policy"]
56+
payload["policy_id"] = policy.get("id")
57+
payload["policy_digest"] = policy.get("digest")
58+
59+
# Optional: map authorization -> holder_binding (AIP-0003)
60+
if signet_receipt.get("authorization"):
61+
payload["holder_binding"] = {
62+
"mode": "jwk_thumbprint", # Adjust based on Signet's auth shape
63+
"thumbprint": jwk_thumbprint(signet_receipt["authorization"]),
64+
}
65+
66+
# Re-sign under draft-02 canonicalization (JCS RFC 8785, AIP-0001 ASCII-keys)
67+
canonical = jcs_canonicalize(payload)
68+
signature_bytes = operator_signer.sign(canonical)
69+
70+
return {
71+
"payload": payload,
72+
"signature": {
73+
"alg": "EdDSA",
74+
"kid": jwk_thumbprint(operator_signer.public_key()),
75+
"sig": base64url_encode(signature_bytes).rstrip("="),
76+
},
77+
}
78+
```
79+
80+
## Verification after adapt
81+
82+
```bash
83+
# Write adapted envelope
84+
echo "$DRAFT02_ENVELOPE" > /tmp/signet-as-draft02.json
85+
86+
# Publish operator pubkey as JWKS at well-known URL, then:
87+
npx @veritasacta/verify /tmp/signet-as-draft02.json --jwks https://operator.example/jwks
88+
89+
# Or pin the key locally:
90+
npx @veritasacta/verify /tmp/signet-as-draft02.json --key ./operator-public.pem
91+
```
92+
93+
Expected output on a successful adapt + verify:
94+
95+
```
96+
✓ Signature valid
97+
✓ Issuer: signet:operator:...
98+
✓ Decision: allow
99+
✓ Issued: 2026-04-19T...
100+
✓ Tier: T1 (basic)
101+
```
102+
103+
## Alternative: transcode mode (no re-sign)
104+
105+
If the operator specifically wants the SAME Ed25519 signature to verify under BOTH Signet and draft-02 rules, the transcoding is stricter: the canonicalization MUST produce the same byte string under both systems, which in practice requires the Signet canonical form and the draft-02 payload canonical form to be byte-identical.
106+
107+
That is achievable but constrains Signet's field set (cannot include fields that draft-02 does not canonicalize, and vice versa). Given the known deltas on `params` (redacted in draft-02), this mode is not straightforward.
108+
109+
**Co-sign mode is the recommended default.** Transcode mode is a design conversation for Path 3 (native alignment), not for an adapter.
110+
111+
## Open questions for the Signet maintainer
112+
113+
1. Does a co-sign adapter make sense as a Signet feature flag (emit both Signet-native and draft-02 envelopes per operation), or as an out-of-tree tool?
114+
2. Would a Rust-native adapter (`signet-draft02-adapter` crate) be accepted upstream into the Signet workspace?
115+
3. Is the operator's private key accessible at the adapter site, or is co-signing constrained by key-custody boundaries?
116+
117+
The answers shape whether the adapter lives in the Signet repo, this repo, or as a third-party crate.

conformance/signet/DEVIATIONS.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Signet wire-format deviations from draft-farley-acta-signed-receipts-02
2+
3+
Four deltas that require adapter handling but do not affect semantic conformance. Each is adaptable with under ~20 lines of code per direction.
4+
5+
## 1. Envelope shape: flat struct vs `{payload, signature}` split
6+
7+
**Signet.** Single flat struct with `sig` as one field among the others:
8+
9+
```json
10+
{
11+
"v": 1,
12+
"id": "...",
13+
"action": {"tool": "...", ...},
14+
"signer": {"pubkey": "ed25519:..."},
15+
"ts": "...",
16+
"sig": "ed25519:..."
17+
}
18+
```
19+
20+
**draft-02.** Envelope splits the signed material from the signature metadata:
21+
22+
```json
23+
{
24+
"payload": {
25+
"action": "...",
26+
"issuer_id": "...",
27+
"issued_at": "..."
28+
},
29+
"signature": {
30+
"alg": "EdDSA",
31+
"kid": "...",
32+
"sig": "..."
33+
}
34+
}
35+
```
36+
37+
**Adapter direction (Signet -> draft-02):** Move Signet's `sig` out into `signature.sig` (stripping the `ed25519:` prefix); derive `signature.kid` from `signer.pubkey` (see deviation 4); lift all other Signet top-level and nested fields under `payload`; set `signature.alg = "EdDSA"`.
38+
39+
**Impact on conformance:** None. JCS canonicalization reproduces the same byte string for identical field sets regardless of nesting, as long as the adapter includes all originally-signed fields under `payload`.
40+
41+
## 2. Signature encoding: `ed25519:<base64>` vs `<base64url>`
42+
43+
**Signet.** Signature is a string of the form `ed25519:<standard-base64>` (canonical padded base64).
44+
45+
**draft-02.** Signature is base64url (URL-safe alphabet, no padding), no algorithm prefix. Algorithm is carried in `signature.alg`.
46+
47+
**Rationale for Signet's form.** Prefix-based encoding is self-identifying and forward-compatible with algorithm rotation (a post-quantum variant would be `ml-dsa-65:<base64>`). This is a reasonable design choice; draft-02 achieves the same via the separate `alg` field.
48+
49+
**Adapter direction (Signet -> draft-02):** Strip the `ed25519:` prefix, decode the base64 body, re-encode as base64url without padding, place in `signature.sig`. Set `signature.alg = "EdDSA"`.
50+
51+
**Possible path 3 (native alignment):** draft-02 could optionally accept a prefixed signature form in a future revision, matching Signet's convention. Discussion welcome.
52+
53+
## 3. Chain linkage: parent ID vs parent hash
54+
55+
**Signet.** `action.parent_receipt_id` is the **ID** of the prior receipt.
56+
57+
**draft-02.** `payload.previousReceiptHash` is the **SHA-256 hash of the canonicalized prior envelope**.
58+
59+
**Tradeoff.** Signet's form is shorter and human-linkable; draft-02's form is tamper-evident without needing access to the prior receipt. A verifier given only a chain tip can detect tampering in any intermediate receipt by recomputing hashes; with Signet's IDs, an attacker can substitute a tampered receipt with the same ID and the chain still "links".
60+
61+
**Adapter direction (Signet -> draft-02):** Compute the SHA-256 of the canonicalized Signet prior receipt at adapter time and emit as `previousReceiptHash`. The adapter MUST have access to the prior receipt (or its hash) to emit this field.
62+
63+
**For Signet self-certification:** this is the most significant semantic delta. Two reasonable positions:
64+
65+
1. **Declare partial conformance at T1 minus chain-linkage.** Signet receipts verify as individual signed artifacts but do not carry tamper-evident chain linkage in the draft-02 sense. An operator wanting tamper-evident chain linkage uses the adapter.
66+
2. **Add a `parent_hash` field alongside `parent_receipt_id`.** Dual-mode: Signet-native verifiers keep using IDs; draft-02 verifiers use the hash. Zero-cost additive change.
67+
68+
Maintainer preference welcome.
69+
70+
## 4. Key identifier: pubkey string vs JWK thumbprint
71+
72+
**Signet.** `signer.pubkey = "ed25519:<base64-encoded-raw-pubkey>"`. The pubkey is carried directly; verification uses it by decoding.
73+
74+
**draft-02.** `signature.kid` is the RFC 7638 JWK thumbprint of the operator's public key. The pubkey itself is NOT in the receipt. Verifiers resolve the pubkey from an external source (`--jwks`, `--key`, or a pinned trust anchor).
75+
76+
**Rationale for draft-02's form.** Embedding the verification key in the receipt body does not protect against tampering. The key distribution concern is documented in Section 9 of draft-02 ("Key Distribution and Trust Anchors") and was the driver of the 0.4.0 embedded-key rejection across the protect-mcp and @veritasacta/verify stack.
77+
78+
**Adapter direction (Signet -> draft-02):**
79+
80+
- The adapter computes a JWK thumbprint over the Signet pubkey at adapt time and emits it as `signature.kid`.
81+
- The adapter does **not** emit the raw pubkey in the draft-02 envelope.
82+
- The adapter SHOULD publish the pubkey at an out-of-band discovery endpoint (e.g. operator-signed JWKS) for the verifier to resolve via `--jwks`.
83+
84+
**For Signet self-certification:** this is where the conformance bar has teeth. A Signet receipt with the raw pubkey in the payload would fail draft-02 verification on key-distribution grounds. The adapter-produced envelope MUST omit the raw pubkey and MUST carry only the thumbprint.
85+
86+
Not a blocker for conformance; just a real constraint on what the adapter emits.
87+
88+
## Summary
89+
90+
| Delta | Severity | Impact on adapter |
91+
|---|---|---|
92+
| Envelope shape | Structural | ~10 lines, boilerplate. |
93+
| Signature encoding | Cosmetic | ~5 lines, strip prefix + re-encode. |
94+
| Chain linkage (ID vs hash) | Semantic | Most significant. Either accept partial conformance or add a dual `parent_hash` field in Signet. |
95+
| Key identifier (pubkey vs thumbprint) | Policy | Adapter MUST NOT emit raw pubkey. External key discovery is the verification path. |
96+
97+
None of these deltas are structural blockers to conformance. All are adapter-addressable; two could be additionally softened by thin Signet-side extensions.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Signet -> draft-farley-acta-signed-receipts-02 field mapping
2+
3+
**Status:** Template. Signet maintainer sign-off required on each `[CONFIRM]` marker.
4+
5+
All field mappings below are inferred from the public Signet source at [Prismer-AI/signet/crates/signet-core/src/](https://github.com/Prismer-AI/signet/tree/main/crates/signet-core/src).
6+
7+
## Top-level structure
8+
9+
| Signet receipt | draft-02 envelope | Notes |
10+
|---|---|---|
11+
| Flat struct with `sig` field | `{payload: {...}, signature: {alg, kid, sig}}` | Structural delta. Requires adapter. See [`DEVIATIONS.md`](./DEVIATIONS.md). |
12+
| `v` (u8) | (no equivalent) | Signet receipt version. Not mapped; the draft relies on `algorithm` and the IETF draft version for versioning. |
13+
| `sig` (String, `ed25519:<base64>`) | `signature.sig` (base64url) + `signature.alg = "EdDSA"` | Encoding delta. See [`DEVIATIONS.md`](./DEVIATIONS.md). |
14+
15+
## Payload field mapping
16+
17+
| Signet field | draft-02 field | Required in draft-02? | Notes |
18+
|---|---|---|---|
19+
| `action.tool` | `payload.action` or `payload.tool_name` | REQUIRED | Tool identifier. draft-02 accepts a string; Signet stores the same. |
20+
| `action.params` | (no equivalent) | - | draft-02 redacts parameter values by default (privacy). See §3 of the draft. Signet carrying the raw `params` is a superset: the adapter drops this field before signing the draft-02 payload. |
21+
| `action.params_hash` | `payload.action_ref` | OPTIONAL | Both are SHA-256 of canonicalized params. `action_ref` is the cross-engine correlation anchor added in draft-02. **Direct semantic match.** `[CONFIRM: is Signet's params_hash a SHA-256 of JCS-canonicalized params? If canonicalization differs, this is a delta.]` |
22+
| `action.target` | (no direct equivalent) | - | Signet-specific resource identifier. `[CONFIRM: can this be expressed as part of the action string, or does it need a new payload extension?]` |
23+
| `action.transport` | `payload.transport_hint` | OPTIONAL (draft-03) | transport_hint accepts `direct`, `ohttp`, `tor`, `custom`. `[CONFIRM: what values does Signet's transport field take, and do any require a new enum variant?]` |
24+
| `action.session` | (no equivalent) | - | `[CONFIRM: is this an agent session ID? If yes, could map to `agent_id` scoped to a session suffix.]` |
25+
| `action.call_id` | (no direct equivalent) | - | `[CONFIRM: is this an external correlation ID? Could map to an application-scoped extension field.]` |
26+
| `action.response_hash` | (no equivalent in a decision receipt) | - | draft-02 decision receipts are signed BEFORE the action executes. Signet pattern of carrying `response_hash` in the same receipt suggests a post-execution receipt variant; `[CONFIRM: does Signet emit one receipt or two (pre + post)? If two, only the pre-receipt is comparable to draft-02 decision receipts.]` |
27+
| `action.trace_id` | `payload.iteration_id` | OPTIONAL | `iteration_id` is the draft-02 field for logical grouping of multi-step workflows. **Direct semantic match.** |
28+
| `action.parent_receipt_id` | `payload.previousReceiptHash` | OPTIONAL from receipt 2+ | Chain linkage delta. Signet links by receipt ID; draft-02 links by SHA-256 of the prior canonical envelope. See [`DEVIATIONS.md`](./DEVIATIONS.md). |
29+
| `signer.pubkey` (`ed25519:<base64>`) | `signature.kid` (JWK thumbprint per RFC 7638) | REQUIRED | Key identifier format delta. See [`DEVIATIONS.md`](./DEVIATIONS.md). `[CONFIRM: is the adapter expected to compute a JWK thumbprint from the Signet pubkey at conversion time?]` |
30+
| `signer.name` | `payload.issuer_id` | OPTIONAL | Operator identity. |
31+
| `signer.owner` | (no direct equivalent) | - | `[CONFIRM: is this a multi-level identity (owner -> named signer)? Could map to the `holder_binding` extension with mode=`jwk_thumbprint`.]` |
32+
| `authorization` | `payload.holder_binding` (AIP-0003) | OPTIONAL | Both represent delegated authority. Signet's `Authorization` struct contents need to match one of the `holder_binding` modes: `jwk_thumbprint`, `dpop`, or `attested_credential`. `[FILL: what fields does Signet's Authorization carry? A direct dump of the Rust struct would resolve this.]` |
33+
| `policy` (PolicyAttestation) | `payload.policy_digest` + `payload.policy_id` | OPTIONAL | `[FILL: what fields does Signet's PolicyAttestation carry? If it is a policy-pack ID + digest, the mapping is trivial. If it carries evaluation evidence, map to `attestation_mode`.]` |
34+
| `ts` (RFC 3339) | `payload.issued_at` (ISO 8601) | REQUIRED | RFC 3339 is a subset of ISO 8601, formats are compatible at the wire level. |
35+
| `exp` (Option<RFC 3339>) | (no equivalent in a decision receipt) | - | Signet receipts have an optional expiration; draft-02 decision receipts do not. `[CONFIRM: is `exp` a soft hint (informational) or a hard verification gate? If the latter, this is a verifier-behavior delta too.]` |
36+
| `nonce` | `payload.nullifier` (VOPRF mode only) | OPTIONAL | Different semantics. `nullifier` in draft-02 is a VOPRF mode artifact (issuer-blind metering); Signet's `nonce` appears to be replay protection. `[CONFIRM: is Signet's nonce a freshness token, a replay counter, or a privacy token? Different answers map to different draft-02 fields or to none.]` |
37+
| `id` | (no direct equivalent; receipt is identified by its hash) | - | Signet assigns each receipt a string ID. draft-02 uses the SHA-256 of the canonical envelope as the identifier. |
38+
39+
## Decision enum
40+
41+
| Signet decision | draft-02 decision | Notes |
42+
|---|---|---|
43+
| `allow` | `allow` | Direct. |
44+
| `deny` | `deny` | Direct. |
45+
| `[FILL]` (any additional) | `require_approval`, `challenge`, `payment_required`, `escalate`, `override`, `compensated` | draft-02 extended enum. `[CONFIRM: does Signet only have allow / deny, or are there additional decision values?]` |
46+
47+
## Mandatory / optional coverage summary
48+
49+
A Signet receipt maps cleanly onto draft-02 **at the envelope level** once the adapter is applied:
50+
51+
- **Directly mappable:** `action.tool`, `action.params_hash`, `action.trace_id`, `action.parent_receipt_id`, `signer.pubkey`, `signer.name`, `ts`, `sig`.
52+
- **Dropped at adapt time (superset):** `action.params` (redacted per draft-02 privacy model), `action.response_hash` (if present and post-execution).
53+
- **Needs maintainer input:** `action.target`, `action.session`, `action.call_id`, `action.response_hash`, `authorization`, `policy`, `exp`, `nonce`, `signer.owner`, decision enum.
54+
55+
Every `[CONFIRM]` / `[FILL]` marker above either resolves to a direct mapping, a deviation (documented in [`DEVIATIONS.md`](./DEVIATIONS.md)), or a proposal for a new OPTIONAL draft-02 field (tracked for draft-03 consideration).

0 commit comments

Comments
 (0)