Skip to content

Commit 55c17a0

Browse files
authored
Merge pull request #1 from VeritasActa/conformance/signet-template
conformance: pre-filled Signet self-certification template (for @willamhou review)
2 parents 0fa18bf + 5ab2dd7 commit 55c17a0

5 files changed

Lines changed: 309 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-certified (adapter crate pending) | @willamhou | Mapping confirmed by @willamhou in [agt-integration-profile#1](https://github.com/VeritasActa/agt-integration-profile/pull/1); conformance/signet/ captures the agreed field mapping and four documented wire-format deltas. Signet is adding `parent_hash` alongside `parent_receipt_id` and shipping a `--emit-draft02` flag plus an in-workspace `signet-draft02-adapter` crate in an upcoming point release. Cross-documentation at [aeoess/agent-governance-vocabulary#37](https://github.com/aeoess/agent-governance-vocabulary/pull/37). |
1010

1111
## How to add an implementation
1212

conformance/signet/ADAPTER.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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. The Signet maintainer has confirmed the co-signer shape below and committed to shipping this as a `--emit-draft02` flag on `signet sign` (main repo), plus an in-workspace `signet-draft02-adapter` crate. Production implementation is scheduled on the Signet side; this file remains as the architectural reference. Open questions at the bottom are resolved.
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+
# Map PolicyAttestation -> policy_digest / policy_id / decision.
54+
# Signet's PolicyAttestation struct uses policy_name and policy_hash.
55+
if signet_receipt.get("policy"):
56+
policy = signet_receipt["policy"]
57+
payload["policy_id"] = policy.get("policy_name")
58+
payload["policy_digest"] = policy.get("policy_hash")
59+
if policy.get("decision"):
60+
payload["decision"] = policy["decision"]
61+
# Suggested: attestation_evidence extension for richer fields
62+
if policy.get("matched_rules") or policy.get("reason"):
63+
payload["attestation_evidence"] = {
64+
"matched_rules": policy.get("matched_rules", []),
65+
"reason": policy.get("reason"),
66+
}
67+
68+
# Map Authorization -> holder_binding (AIP-0003).
69+
# Signet's Authorization struct: { chain, chain_hash, root_pubkey }.
70+
# Only chain_hash and root_pubkey enter the signed scope; full chain
71+
# is carried for storage and dropped at adapt time.
72+
if signet_receipt.get("authorization"):
73+
auth = signet_receipt["authorization"]
74+
payload["holder_binding"] = {
75+
"mode": "jwk_thumbprint",
76+
"thumbprint": jwk_thumbprint_of_ed25519_pubkey(auth["root_pubkey"]),
77+
"delegation_chain_hash": auth["chain_hash"],
78+
}
79+
80+
# Re-sign under draft-02 canonicalization (JCS RFC 8785, AIP-0001 ASCII-keys)
81+
canonical = jcs_canonicalize(payload)
82+
signature_bytes = operator_signer.sign(canonical)
83+
84+
return {
85+
"payload": payload,
86+
"signature": {
87+
"alg": "EdDSA",
88+
"kid": jwk_thumbprint(operator_signer.public_key()),
89+
"sig": base64url_encode(signature_bytes).rstrip("="),
90+
},
91+
}
92+
```
93+
94+
## Verification after adapt
95+
96+
```bash
97+
# Write adapted envelope
98+
echo "$DRAFT02_ENVELOPE" > /tmp/signet-as-draft02.json
99+
100+
# Publish operator pubkey as JWKS at well-known URL, then:
101+
npx @veritasacta/verify /tmp/signet-as-draft02.json --jwks https://operator.example/jwks
102+
103+
# Or pin the key locally:
104+
npx @veritasacta/verify /tmp/signet-as-draft02.json --key ./operator-public.pem
105+
```
106+
107+
Expected output on a successful adapt + verify:
108+
109+
```
110+
✓ Signature valid
111+
✓ Issuer: signet:operator:...
112+
✓ Decision: allow
113+
✓ Issued: 2026-04-19T...
114+
✓ Tier: T1 (basic)
115+
```
116+
117+
## Alternative: transcode mode (no re-sign)
118+
119+
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.
120+
121+
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.
122+
123+
**Co-sign mode is the recommended default.** Transcode mode is a design conversation for Path 3 (native alignment), not for an adapter.
124+
125+
## Signet maintainer answers
126+
127+
Resolved in the review of [VeritasActa/agt-integration-profile#1](https://github.com/VeritasActa/agt-integration-profile/pull/1):
128+
129+
1. **Feature flag.** The co-signer emits as a `--emit-draft02` flag on `signet sign`. When set, both the Signet-native envelope and the draft-02 envelope are produced per operation. Lives in the main Signet repo (discoverable, tested in the main CI).
130+
131+
2. **In-workspace adapter crate.** A `signet-draft02-adapter` crate is welcome inside the Signet Cargo workspace. Timing depends on the conformance bar; maintainer will prioritise after field mapping is confirmed (confirmed by this PR).
132+
133+
3. **Co-signer mode is the right default.** The operator private key is accessible at the adapter site, so the adapter can produce a draft-02 envelope that verifies under the operator's public key. No cross-custody complications at the adapter site.
134+
135+
The adapter therefore lives in the Signet main repo (not this repo, not third-party). This repo retains the architectural reference (this file) and the normative field mapping ([`FIELD-MAPPING.md`](./FIELD-MAPPING.md)).

conformance/signet/DEVIATIONS.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
**Maintainer preference (selected).** The Signet maintainer has chosen **Option 2**: add a `parent_hash` field alongside `parent_receipt_id`. Dual-mode, additive, low-effort. Both fields coexist on the wire. Signet-native verifiers continue to use `parent_receipt_id`; draft-02 verifiers use `parent_hash`.
64+
65+
**Signet-side implementation note.** This requires the signer to have access to the prior receipt's canonical bytes at sign time; the signer API needs a minor extension to accept (or compute) the parent hash. Scheduled for an upcoming Signet point release. Once shipped, this deviation collapses to "emit `parent_hash` on the wire, read it on the draft-02 side", and the adapter's chain-linkage step becomes zero-cost.
66+
67+
## 4. Key identifier: pubkey string vs JWK thumbprint
68+
69+
**Signet.** `signer.pubkey = "ed25519:<base64-encoded-raw-pubkey>"`. The pubkey is carried directly; verification uses it by decoding.
70+
71+
**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).
72+
73+
**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.
74+
75+
**Adapter direction (Signet -> draft-02):**
76+
77+
- The adapter computes a JWK thumbprint over the Signet pubkey at adapt time and emits it as `signature.kid`.
78+
- The adapter does **not** emit the raw pubkey in the draft-02 envelope.
79+
- 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`.
80+
81+
**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.
82+
83+
Not a blocker for conformance; just a real constraint on what the adapter emits.
84+
85+
## Summary
86+
87+
| Delta | Severity | Impact on adapter |
88+
|---|---|---|
89+
| Envelope shape | Structural | ~10 lines, boilerplate. |
90+
| Signature encoding | Cosmetic | ~5 lines, strip prefix + re-encode. |
91+
| Chain linkage (ID vs hash) | Semantic | Resolved. Signet is adding `parent_hash` alongside `parent_receipt_id` in an upcoming point release; dual-mode, zero-cost after the point release ships. |
92+
| Key identifier (pubkey vs thumbprint) | Policy | Adapter MUST NOT emit raw pubkey. External key discovery is the verification path. |
93+
94+
None of these deltas are structural blockers to conformance. All are adapter-addressable; two could be additionally softened by thin Signet-side extensions.

0 commit comments

Comments
 (0)