Skip to content

spec(evidence): signer-key resolution (v0.2 additive draft layer)#112

Merged
amavashev merged 7 commits into
mainfrom
feat/evidence-signer-key-resolution
Jun 15, 2026
Merged

spec(evidence): signer-key resolution (v0.2 additive draft layer)#112
amavashev merged 7 commits into
mainfrom
feat/evidence-signer-key-resolution

Conversation

@amavashev

@amavashev amavashev commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

What

Adds the signer-key resolution layer to the CyclesEvidence draft as an
additive v0.2 — the (b) half of the signature story (signature authority),
where APS#45 already shipped (a) (signature validity). Draft-only; no merged
artifacts change.

This is the spec follow-up to the design agreed on #103 / aeoess#43.

Changes (drafts/cycles-evidence-v0.1.yaml)

  • signer_did now has two valid forms: the v0.1 raw 64-hex Ed25519 pubkey
    (still valid — nothing breaks), OR a resolvable
    did:cycles:<sha256(server_id)>#<kid>.
  • New CyclesEvidenceJwks / CyclesEvidenceJwk schemas — the
    server_id-relative JWK Set published at
    GET {server_id}/.well-known/cycles-jwks.json. Ed25519 OKP keys carrying
    cycles_nbf_ms / cycles_exp_ms validity windows + advisory status;
    rotation MUST keep retired keys in the set.
  • Normative selection rule: pick the key whose [nbf, exp) window covers the
    envelope's issued_at_ms — explicitly not "the current key" (that naive
    impl breaks archival verification after a rotation).
  • The raw-hex ↔ JWK x bridge, and the five distinct verify dispositions:
    authentic / binding_only / signer_authority_failed / signer_resolution_failed / signature_invalid
    — resolution-failure and invalid-signature MUST stay distinct (a network
    failure is not a forgery).
  • Out-of-scope note + cycles-spec-index.yaml evidence_envelope entry updated:
    the layer is now specified, and promotes to cycles-evidence-v0.2.yaml on the
    normative cut.

Validation

  • spectral lint: 0 errors (one expected oas3-unused-component warning for the
    not-yet-referenced JWKS schema)
  • merge-drift: clean (draft does not feed merged/)
  • changelog pointers: valid
  • YAML parses

Review ask

@aeoess — this is the layout posted to aeoess#43. Flagging the member names
(cycles_nbf_ms / cycles_exp_ms / status) and the window semantics before the
v0.2 normative cut, in case anything is awkward for the APS resolver. Not merging
until the design has a thumbs-up.

Refs: #103, aeoess#43

Per the design agreed with APS on cycles-protocol#103 / aeoess#43. Adds the
signature-AUTHORITY layer (the (b) half; (a) signature-validity already ships
in APS #45), additive over v0.1 so raw-hex signer_did + expected_signer pinning
keep working unchanged.

- signer_did: promote to TWO valid forms — raw 64-hex pubkey (v0.1) OR
  did:cycles:<sha256(server_id)>#<kid> (resolvable).
- New CyclesEvidenceJwks / CyclesEvidenceJwk schemas: the server_id-relative
  JWK Set (GET {server_id}/.well-known/cycles-jwks.json), Ed25519 OKP keys with
  cycles_nbf_ms / cycles_exp_ms / status; MUST include retired keys.
- Normative WINDOW selection rule (pick the key whose [nbf,exp) covers
  issued_at_ms — never "the current key"; that easy impl breaks archival
  verification), the raw-hex<->JWK x bridge, and the four distinct verify
  dispositions (authentic / binding_only / signer_resolution_failed /
  signature_invalid — the latter two MUST stay distinct: a network failure is
  not a forgery).
- Out-of-scope note + spec-index evidence_envelope entry updated to reflect the
  layer is now specified (moves to cycles-evidence-v0.2.yaml on the normative cut).

Draft-only; spectral 0 errors, no merged drift, changelog valid.
Five findings from the APS maintainer on the #43 design comment, all applied
to the draft:

- Add `signer_authority_failed` disposition (Medium): resolution SUCCEEDED but
  the key is not authorized — DID↔server_id hash mismatch, no window-covering
  key, raw-hex key absent from the set, ambiguous/duplicate-kid selection. Kept
  distinct from `signer_resolution_failed` (fetch failed) and `signature_invalid`
  (forgery): a successful-fetch-but-unauthorized case is neither.
- Redefine `binding_only` vs PR #45 (Medium): now "signature valid, authority
  not established," subsuming PR #45's `signature_valid`; an `issuer_pin_matched`
  companion boolean carries the `pinned_issuer` facet. Closes the
  unpinned-valid gap.
- Make the .well-known path explicitly server_id-relative / API-base-relative,
  NOT RFC 8615 origin (Medium): stated intent + rationale — server_id (path
  included) is the hash anchor, pure string append avoids the origin-parsing
  normalization split, and allows co-hosting.
- Define exact server_id_hash input bytes (Low): sha256 over the verbatim UTF-8
  server_id string, NO URL normalization; producer emits one canonical form.
- Add deterministic JWK selection rules (Low): kid uniqueness, duplicate-kid /
  overlapping-window / multiple-raw-hex-match → authority error; malformed x /
  missing cycles_nbf_ms → excluded; unparseable set → resolution error.

Draft-only; spectral 0 errors, no merged drift.
@amavashev

amavashev commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Updated per APS review on aeoess#43 (commit be1796a, refined through aec0536). The disposition model is now five, not four — added signer_authority_failed (resolution succeeded but key unauthorized: hash mismatch / no covering key / raw-hex absent / ambiguous-kid), clarified binding_only as the "signature valid, authority not yet established" result with a signer_pin_matched companion boolean, made the .well-known path explicitly server_id/API-base-relative with rationale, pinned the exact sha256(server_id) input bytes (verbatim UTF-8, no normalization), and added deterministic JWK selection rules. Full point-by-point in aeoess#43.

Edited (was stale): an earlier version of this comment said binding_only was "redefined to subsume PR #45 with an issuer_pin_matched companion." That conflated two trust anchors. Corrected in the spec text and above: the companion boolean is signer_pin_matched and records a Cycles-side pin of the Cycles envelope signer (signer_did) — a different anchor from APS PR #45's pinned_issuer, which pins the APS receipt issuer. binding_only corresponds to PR #45's signature-validity result only.

…s#43)

Second APS review pass on the signer-key resolution draft:

- High — `issuer_pin_matched` was wrongly equated with APS PR #45's
  `pinned_issuer`. They are DIFFERENT trust anchors: `issuer_pin_matched` is
  this (Cycles) verifier pinning the Cycles ENVELOPE signer (`signer_did`);
  PR #45's `pinned_issuer` pins the APS RECEIPT issuer (the APS object's own
  `signer`). Reworded to state the Cycles-side anchor and call out the
  distinction explicitly; dropped the "subsumes pinned_issuer facet" claim.
- Medium — OAS 3.1.0 nullability: `cycles_exp_ms` switched from the OAS-3.0
  `nullable: true` to JSON-Schema `type: [integer, "null"]` so tooling accepts
  the documented absent/null.
- Low — `alg`: clarified it is OPTIONAL (RFC 7517) but a present-but-non-EdDSA
  value makes the JWK invalid and excludes it from candidacy, same as a wrong
  `kty`/`crv`; absent `alg` is fine. Added to both the field and the
  deterministic-selection rule.
- Low — spec-index `evidence_envelope` owns-list said "four verify
  dispositions"; corrected to five (now naming all of them).

Draft-only; spectral 0 errors, no merged drift.
…ss#43)

The companion boolean on `binding_only` records a Cycles-side pin of the
Cycles ENVELOPE signer (`signer_did`); "issuer" read like APS PR #45's
receipt-issuer anchor (a different trust anchor). `signer_pin_matched` is
clearer while the text is still draft. No semantic change.
…s#43)

After the issuer_pin_matched -> signer_pin_matched rename, the v0.1
out-of-scope note still described expected_signer as "pinning for issuer
trust", which reintroduces the APS receipt-issuer ambiguity. expected_signer
pins the Cycles ENVELOPE signer; reworded accordingly. (The one remaining
"issuer" is the deliberate reference to APS PR #45's receipt-issuer anchor.)
…WKS path (aeoess#43)

APS signed off on keeping the .well-known path API-base-relative. Folding their
reasoning into the spec text so the choice reads as deliberate:

- Lead with AUTHORITY SCOPE (the decisive reason): the did:cycles hash covers
  server_id with its path, so origin-rooting would let the host-root principal
  answer for a signer whose DID only committed to {host}/v1 — a cross-tenant
  authority broadening on path-multi-tenant hosts. Keeping the JWK Set under the
  same base holds key authority and identity anchor in one scope.
- Keep the determinism reason (pure lexical append, no URL-normalization drift;
  same rule as the hash input).
- Position vs RFC 8414: OAuth AS metadata inserts .well-known host-rooted; OIDC's
  append-after-path is the interop wart. We take append-after-path on purpose
  because host-rooted insertion splits metadata authority from the committed
  path — turns "that's not RFC 8615" into a one-line answer for a standards
  reviewer.
- Note the literal .well-known segment is kept for its "this is metadata, do not
  route it" signal.

Draft-only; spectral 0 errors, no merged drift.
…s-authority wording (aeoess#43)

Two APS review findings on #112:

- Medium — CyclesEvidenceJwks had additionalProperties:false, contradicting the
  "RFC 7517 JWK Set" framing. RFC 7517 §5 lets a JWK Set carry extra top-level
  members and requires ignoring unknown ones (the child CyclesEvidenceJwk already
  does this). A harmless set-level extension would have spuriously failed schema
  validation. Set to true with a note.
- Low — signer_resolution_failed said "the kid lookup could not be
  resolved/reached", which an implementer could read as mapping a missing kid in
  a fetched set to resolution failure — but the deterministic-selection rule
  already classes that as signer_authority_failed. Reworded: resolution_failed is
  about OBTAINING the set (DID method / fetch / parse), not searching it; a
  missing/duplicate kid or no covering key in a parsed set is authority_failed.

Draft-only; spectral 0 errors, no merged drift.
@amavashev amavashev merged commit a62c73b into main Jun 15, 2026
5 checks passed
@amavashev amavashev deleted the feat/evidence-signer-key-resolution branch June 15, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant