Dev auths network#271
Merged
Merged
Conversation
…519N witness keys) KERI witnesses are conventionally non-transferable identifiers: their verkey carries the CESR code B (Ed25519N) rather than D (Ed25519). Previously KeriPublicKey::Ed25519 was a bare [u8; 32] tuple whose is_transferable() was hardcoded to true, and parse() rejected the B code outright — so a receipt signed by a standard witness could not be verified even though the raw key and Ed25519 algorithm are identical to a working D key. Mirror the existing P-256 1AAJ/1AAI modeling on Ed25519: the variant now carries the transferability recorded from its code (D = transferable, B = not), so parse() accepts B, is_transferable()/cesr_prefix() are truthful, and to_qb64() round-trips B correctly. verify_signature() is curve-only and unchanged. Readers that built or matched the old tuple variant are updated; raw-byte constructors route through KeriPublicKey::ed25519() (one construction path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
auths-keri carried only an auths-internal KeyStateNotice
({version,t:"ksn",state,dt}), not the canonical KERI key-state record
({vn,i,s,p,d,f,dt,et,kt,k,nt,n,bt,b,c,ee,di}) that keripy/keriox publish
and consume — so a thin client could neither read a peer's key state nor
publish one a peer could read.
Add a typed KeyStateRecord beside the internal envelope: the KERI wire
shape, reusing the existing KeriSequence (lowercase-hex) and Threshold
(hex/clause) serializers and an `ee` establishment sub-record {s,d,br,ba}.
from_kel replays a KEL into the record (emit); into_key_state projects a
peer's record back to KeyState (ingest) and is total — a parsed record
already carries every field a KeyState needs. The `auths key-state` CLI
(alias `ksn`) wires both directions: --from-kel emits, --ingest consumes.
The emitted record is byte-identical to keripy's reference KeyStateRecord
(same field order, labels, values), so an auths key-state reads in
keripy/keriox and theirs ingests here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…de feature Add auths-witness-node, a feature-gated crate that orchestrates running a hardened witness node for operators. It composes the platform's public crate APIs (auths-witness, auths-keri, auths-verifier) and reimplements no protocol: it owns the operation (parsed standup intent, embedded node+monitor compose manifest over the released image, key-custody policy, health URL), not the bytes a stranger verifies. Extend the `auths witness` command with the operator verb set — up/down/status/register/logs. The clap surface always compiles in (thin defs, no heavy deps), so help and parsing are identical in every build; the handler is feature-split: a build with --features witness-node runs the node, a lean default build returns one actionable line pointing operators at the witness build and pulls none of the node's dependencies. The witness-node feature is purely additive — the default cargo tree for auths-cli does not include the node crate, so the lean install stays lean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ode or fails honestly `auths witness up` now performs a genuine embedded-Compose standup instead of printing a manifest and exiting 0: it brings the node + monitor sidecar up, waits until the node answers its health endpoint, prints that URL, and exits 0. Crucially it FAILS HONESTLY — non-zero exit, one actionable line, nothing left half-standing — when it cannot stand a node up, rather than claiming a success that reality contradicts. - auths-witness-node::standup — the runtime: stand_up()/tear_down() over a ContainerEngine port and a HealthCheck port (ports/adapters; success is a node answering, not the command returning). One-line StandupError on every failure; a failed bring-up tears down what started. - auths-witness-node::engine — shipped adapters: DockerEngine (docker compose, failures distilled to one line) and SocketHealthCheck (dependency-free raw socket GET — all it needs is whether the node answers 2xx). - auths witness up: calls the real standup; adds --image (pin a released tag / run an air-gapped image). down tears down the per-port project; status reports healthy only if a node actually answers; down gains --port. The witness-node feature stays additive (default cargo tree pulls no node crate); the node crate composes the platform crates and reimplements no protocol; the standup manifest declares a released image, never a source build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`auths witness up` now stands up a real witness node that answers its health URL, instead of failing on an unobtainable image and an un-bootable manifest. Three fixes: - deploy/witness Dockerfile: resolve the static musl target from the build's TARGETARCH (amd64/arm64) and add it to the toolchain AFTER the source copy, so the toolchain that rust-toolchain.toml selects has a musl std. The released witness image previously could not build at all (E0463 on a different toolchain instance; x86_64 hardcoded on arm64). - auths-witness-node: the embedded standup manifest dropped a transparency-log monitor sidecar that was the wrong daemon for a single-node standup and pinned an unshippable image — it blocked the whole `compose up`. The manifest is now witness-only and boots healthy. - auths-witness-node: `up` mints the node's stable signing identity at first boot — a 32-byte OS-CSPRNG seed pinned beside the manifest and injected into the node, never a key file baked into the image. Re-run reuses the existing identity. The node still composes the platform's public crates and reimplements no protocol; the witness-node feature stays additive (rand/hex are pulled only with the feature). cargo build/clippy clean (-D warnings), 15 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A witness receipt is only corroboration if a third party who does not
trust the node can check it alone — on a clean machine, with no network
and no registry. The witness's published did:key identity embeds its
verification key, so {receipt, signature, identity} is self-contained.
- auths-verifier: verify_receipt_offline recovers the witness key from
the published did:key and checks the signature over the canonical
receipt bytes, returning a parsed Verified / SignatureFailed /
UnreadableIdentity verdict (Verified is the only success arm).
- auths-witness-node: ReceiptBundle composes that surface — it
reimplements no protocol.
- auths-cli: auths witness verify-receipt --receipt <file> exposes it
(handler feature-gated; the lean default returns the install hint).
A tampered or foreign receipt fails closed with a distinct reason. The
default build pulls no node crate; clippy clean (-D warnings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The witness server retained receipts and first-seen SAIDs but discarded the
event bodies, so it could not recover an identity's current key-state (keys,
thresholds, next-commitment) to serve a thin client. Retain each verified
event body (a new auths-core witness storage events table, first-seen-wins)
and serve the current key-state at a stable endpoint, GET
/witness/{prefix}/key-state, as a KERI-conformant KeyStateRecord
({vn,i,s,p,d,f,dt,et,kt,k,nt,n,bt,b,c,ee,di}) — built only via the trust
kernel's own auths_keri::KeyStateRecord::from_kel after a TrustedKel replay,
never a hand-rolled serializer. The endpoint 404s when the witness has
corroborated no events for the prefix (it cannot notice a key-state it never
saw).
Add KeyStateRecord::sequence()/check_not_stale(last_seen) (returning the
existing KsnError::Stale) and an auths key-state --ingest --reject-stale-below
<hex> flag that composes it, so a verifier already holding a newer state fails
closed on a rewind to an older notice, with a distinct reason.
The served record reconstructs byte-for-byte inside the keripy reference
implementation, and a keripy-published notice ingests on the node — the wire
shape is bidirectionally interoperable, on the running node.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tation) A witness operator vouches for the network; the operator must in turn be vouchable. A running witness node now exposes a signed version+digest build attestation and `auths witness status` verifies it — a forged attestation (validly signed, but over a different binary) is rejected. - auths-core witness server: the node measures its own running binary (sha256 of /proc/self/exe at startup) and serves a BuildProof at GET /build pairing that self-measurement with the signed `auths artifact sign` attestation. 404 when no attestation was configured — a node that cannot prove its binary says so plainly; the server interprets nothing. - auths-verifier: verify_build_attestation_offline — a two-leg, fail-closed verdict: the signature verifies against the key the self-describing did:key issuer embeds, AND the attested digest equals the running digest. A valid signature over the wrong binary lands on DigestMismatch, never Verified. - auths-witness-node: BuildAttestation::verify -> NodeBuildVerdict composes the verifier; a SocketHttpFetch port reads /build. `auths witness status` renders the verdict and fails closed on a forged or absent build. `auths witness up --build-attestation` mounts the released image's attestation into standup. - auths-witness binary: reads AUTHS_WITNESS_BUILD_ATTESTATION at boot and attaches the self-measured proof. The witness-node feature stays additive (no core crate depends on the node crate; the default build pulls none of it). No protocol is hand-rolled — the node composes the platform's public verifier. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A node operator must never need the protocol's wire/ceremony vocabulary (key event logs, key-state notices, self-addressing identifiers, the CESR wire, signing thresholds, and the like) to stand a witness up, check on it, register it, read its logs, or tear it down. That rule was previously three divergent, hand-maintained jargon lists scattered across the crate's tests — each a partial copy free to drift from the surface it guarded. Lift it into one place: a canonical PROTOCOL_VOCABULARY denylist and a whole-word, case-insensitive scan_for_protocol_vocabulary in a new vocabulary module (re-exported from the crate root). Whole-word matching keeps benign operator strings that merely contain the letters — "prefixed", "did:key:...", "received" — from being false positives. The crate's own happy-path tests (health URL, build verdict summary) now consume the canonical scanner; their inline term arrays are deleted. The list covers the full kernel vocabulary, not the earlier six-term subset (adds acdc/tel/verkey/prefix/threshold and more). No operator-facing output changed; the rule keeping it vocabulary-free is now a single enforced contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…keripy A KERI inception has two prefix kinds. A self-addressing AID derives its prefix from the event SAID (i == d), so both d and i are blanked before hashing. A basic AID uses the controlling public key itself as the prefix (i == k[0], e.g. a D-coded Ed25519 verkey); it is NOT self-addressing, so only d is blanked — i stays present in the hashed bytes. compute_said_with_protocol blanked i for EVERY icp/dip/vcp unconditionally, keying only on event type. So for a keripy basic-prefix inception it returned the self-addressing SAID — a confidently-wrong answer (EOoC9Auw… instead of keripy's EAAD4cS7…), silently, rather than matching or rejecting. Fix: blank i only when it is genuinely self-addressing — classify the i value by its CESR derivation code (parse, don't validate) rather than by event type. An empty i (the emit path, before finalize fills i = d), the SAID placeholder, or a digest prefix (E…) is self-addressing; a key prefix (a verkey) is basic and is kept during hashing, exactly as keripy 1.3.4 does. This mirrors the discriminator finalize_icp_event already uses to decide whether to set i = d. auths still emits only self-addressing AIDs (empty i → blanked → i = d unchanged); this is the consume side reading the broader ecosystem's events correctly: write one style, read all. Closes interop gap IOP-L1d. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a did:webs surface so an auths AID resolves under a standard
did:webs/DID-core resolver without the resolver speaking KERI.
auths-keri gains a did_webs module: DidWebsDocument::from_key_state
projects a replayed key-state into the ToIP did:webs reference
didDocument ({id, verificationMethod, service, alsoKnownAs}). The KEL
stays the source of truth — the document is derived, never authored.
Each current signing key becomes one JsonWebKey verification method,
built only from a decoded KeriPublicKey (parse, don't validate); the
fragment is the key's own CESR value (#DAAB…) so it self-identifies
across rotation. PublicKeyJwk projects a key into its curve-correct
JWK: Ed25519 → OKP (x), P-256 → EC (x/y from decompressing the SEC1
point). The JsonWebKey x coordinate is base64url(raw verkey) — byte-
identical to the reference resolver's generate_json_web_key_vm and to
keripy's urlsafe_b64encode(Verfer.raw).
The auths did-webs --from-kel <KEL> --domain <domain> CLI is a thin
adapter over the model (mirroring auths key-state); the crypto/wire
definition lives in auths-keri, never re-implemented in the CLI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ctions
Add an OOBI surface so an auths AID can discover, and be discovered by, KERI
peers — the bootstrap every live KERI exchange (witnessing, credential
presentation, key-state resolution) starts from.
auths-keri::oobi (the I/O-free core, parse-don't-validate):
- Oobi: a parsed OOBI URL (<scheme>://<authority>/oobi/<cid>/<role>[/<eid>]),
keripy's OOBI_RE grammar; every component validated at the boundary.
- Role: a total enum over keripy's kering.Roles vocabulary.
- LocSchemeReply / EndRoleReply: SAID-and-version-correct `rpy` records
({v,t:"rpy",d,dt,r,a}) byte-exact with keripy 1.3.4's Hab.reply
(/loc/scheme {eid,scheme,url} and /end/role/add {cid,role,eid}).
- OobiEndpoint::for_controller: the serve side — derive an AID's OOBI URL and
its rpy reply stream from a replayed key-state.
- ingest_oobi_stream: the resolve side — replay a fetched KEL into a verified
KeyState, bound to the cid the OOBI claimed (a mismatched KEL is rejected,
never silently substituted).
auths-cli `auths oobi`:
- resolve --url [--from-file]: parse a peer OOBI, fetch its bytes (HTTP behind
a port, or an already-fetched stream), replay + print the key-state.
- endpoint --from-kel --authority [--url]: emit the OOBI URL + rpy reply stream
a resolving peer fetches.
Cross-verified both directions against keripy 1.3.4: auths resolves a
keripy-produced KEL, and auths's served /loc/scheme + /end/role/add replies
(SAIDs included) are byte-identical to keripy's Hab.makeLocScheme / Hab.reply.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Issuance & Presentation EXchange (IPEX) handshake — KERI's
standard peer-to-peer way two controllers hand over an ACDC credential,
instead of each inventing a bespoke presentation wire.
- auths-keri::ipex: typed `IpexGrant` / `IpexAdmit` `exn` records. A grant
embeds a saidified ACDC in its `e` block addressed to the recipient; an
admit references the grant's SAID as its prior. Both saidify byte-exact
with keripy 1.3.4 (`keri.vc.protocoling` over `exchanging.exchange`):
field order `{v,t,d,i,rp,p,dt,r,q,a,e}`, the top-level `d` over the whole
record, the grant's `e.d` section SAID over `{acdc,d}`. Parse is total —
wrong route, mismatched record SAID, or a tampered embedded ACDC are all
rejected at the boundary.
- auths ipex grant/admit: a thin file-based CLI adapter over the module
(mirrors `auths oobi` / `auths did-webs`); the transport/signer stay out
of the wire logic.
Cross-verified: keripy 1.3.4 parses an auths-produced grant and recomputes
the identical `exn` SAID, and the grant/admit bytes match keripy's own
`ipexGrantExn` / `ipexAdmitExn` output for the same inputs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xporter) A pairing session runs inside a TLS connection, but the AEAD-sealed SecureEnvelope proofs it carries were transport-agnostic: a proof captured on one TLS connection could be replayed onto a different one (relay/MITM). Bind every proof to its channel via the RFC 9266 `tls-exporter` keying material (RFC 5705): - ChannelBinding: a parsed 32-byte exporter (label EXPORTER-Channel-Binding, absent context), parse-don't-validate, constant-time eq, redacted Debug. - EnvelopeSession::new now requires a ChannelBinding (no unbound constructor); the exporter is folded into the envelope-key HKDF info AND the AAD. A proof sealed on channel A and opened on a session bound to channel B is rejected with ChannelBindingMismatch; a forged binding label still fails the AEAD. - ChannelBindingProvider port, adapted over rustls in the pairing daemon (rustls_channel_binding / RustlsChannelBinding). A TLS 1.3 loopback test proves the exporter is per-session (client==server, differs across sessions). The SecureEnvelope wire format changed, so the cross-impl KAT vectors are regenerated to v2 with a channel_binding_hex input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `auths tls-cert` (issue/verify) and the `auths-keri::tls_cert` module (feature `tls-cert`): an X.509 leaf whose trust roots in a KERI key event log rather than a CA. The leaf carries a `did:keri:<aid>` URI SAN (the SPIFFE X.509-SVID identity-in-SAN pattern) plus a non-critical AuthsKeriBinding extension (OID 1.3.6.1.4.1.59999.1.1) holding the AID's replayed key-state. A stock TLS stack (rustls/OpenSSL/Go crypto/tls) completes a handshake with the leaf; an AID-aware verifier re-derives the trust by replaying the KEL, so an auths identity deploys through every load balancer, mesh, and client that already speaks TLS — augment, not replace. - `AuthsKeriBinding` is parse-don't-validate: built only from a resolved KeyState (undecodable key → error at the boundary), total over its JSON. - The leaf uses a fresh ephemeral TLS keypair, so the AID's long-term signing key never goes on the wire. - `verify_binds_to_key_state` rejects a leaf whose embedded AID / current keys / KEL tip / SAN diverge from a held KEL's replay; a plain cert with no extension is MissingBinding and cannot masquerade as KEL-rooted. - CLI is a thin adapter over the crate (the did-webs/oobi shape); the KEL replay and CESR key-decode remain the single sources of truth. rcgen / x509-parser / yasna are gated behind `tls-cert`, off by default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A KEL-rooted leaf already carries a did:keri:<aid> URI subjectAltName (the SPIFFE X.509-SVID identity-in-SAN shape), but the only way to learn which identity a cert claimed was `tls-cert verify --from-kel`, which requires already holding the AID's KEL. Add the discovery half: read the identity out of the SAN alone, before the KEL is in hand. - auths-keri::tls_cert::extract_aid_from_san reads the did:keri URI from the cert SAN (reusing extract_did_keri_san), strips the scheme, and parses the AID into a validated Prefix. A malformed identifier is InvalidSanAid and a leaf with no did:keri SAN is NoSanIdentity — typed errors at the boundary, never a raw string, so a plain self-signed cert can't masquerade as carrying an auths identity. - `auths tls-cert identity --cert <CERT.pem>` is a thin CLI adapter that prints the did:keri AID a leaf claims: the X.509-SVID "who is this peer?" read, offline, no KEL required. It composes with `verify --from-kel` (resolve the AID, then replay its KEL to root trust in the log). Tests: AID reads out of an issued cert's SAN without the KEL; a plain cert is NoSanIdentity; a malformed did:keri SAN AID is rejected at the parse boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ects forged/revoked/stripped leaves A KEL-rooted leaf previously only bound the AID's replayed key-state; on its own that is not unforgeable — anyone replaying a public KEL could project the same key-state into a leaf minted over their own TLS key, and the verifier accepted it. The leaf now carries a TlsKeyAuthorization: a KERI signature, by one of the AID's current signing keys, over the leaf's SubjectPublicKeyInfo DER. Only the AID's controller can produce it. - TlsKeyAuthorization (parse-don't-validate) rides inside AuthsKeriBinding; both sides re-derive the same SPKI DER (issuer from the keypair, verifier from the parsed cert) so there is one source of truth for what the AID signed. - TlsKeyAuthorizer port: the core never imports a key store; an adapter supplies the signature. issue_authorized_kel_rooted_cert signs the ephemeral TLS key's SPKI and refuses at issuance if the signing key doesn't match the named current key. - verify_authorized_against_key_state is the adversarial verifier: on top of the chain-to-the-log check it requires the authorization and checks it against the replayed current keys. Distinct typed rejections — Unauthorized (forged), MissingAuthorization (stripped), MissingBinding (plain), BindingMismatch (revoked/rotated), SanMismatch (spoof). - CLI: `tls-cert issue --sign-key <AID-KEY.pem>` mints the authorized leaf; `tls-cert verify` is the adversarial verifier (no back-compat). 11 new tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QUIC runs the same TLS 1.3 handshake inside its CRYPTO frames, so a KERI identity composes with QUIC — and therefore HTTP/3 — through exactly the two mechanisms it already composes with TLS over TCP: the KEL-rooted leaf the server presents (re-rooted by replaying the KEL, trust in the log not a CA) and the per-connection channel binding both endpoints export from the connection's TLS 1.3 secrets (RFC 5705; both ends agree, two connections differ → anti-relay). A new `quic` feature on auths-keri adds the QUIC adapter for those two mechanisms (quic_transport): server/client configs built from the KEL-rooted leaf, the quinn::Connection keying-material exporter, and a real loopback driver. `auths tls-cert quic --from-kel` is the thin CLI adapter — it serves the leaf over a loopback QUIC endpoint, completes the handshake (ALPN h3), re-roots the served leaf in the replayed KEL, and proves both endpoints derive the same channel binding. Quinn's public exporter API has no RFC 9266 "absent context" form, so the two auths QUIC endpoints agree on the registered EXPORTER-Channel-Binding label plus an explicit auths-quic context; the per-connection anti-relay property holds regardless, and the distinct context keeps a QUIC and a TCP binding for the same secrets from colliding. quic is off by default — the core KERI types carry no QUIC dependency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests/conformance — a live cross-implementation conformance suite that drives the real auths CLI against the keripy 1.3.4 reference for every KERI interop surface and fails on any byte/structural divergence: - key-state (ksn) emit + ingest - did:webs documents (Ed25519 + P-256 JWK) - OOBI serve (loc-scheme / end-role replies, SAIDs included) - IPEX grant / admit envelopes (top SAID + embeds-SAID) Each surface asserts auths output == keripy oracle (computed live in oracle.py) AND == a frozen golden vector, with a provenance MANIFEST (sha256 + gen_command per vector). Deterministic fixtures; every CLI invocation uses an isolated --repo tmpdir, never ~/.auths. Wire it as a PR gate: new `conformance` job in ci.yml builds the CLI, installs the pinned keri==1.3.4 oracle via uv, and runs the suite on every pull request to main.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Auths Commit Verification
Result: ❌ 0/19 commits verified How to fixCommit 1. Install auths macOS: 2. One-time setup (creates your identity and configures Git) auths init3. Sign this branch and push auths sign origin/main..HEAD
git push --force-with-leaseFor CI to verify the signer, commit an identity bundle: auths id export-bundle --alias main --output .auths/ci-bundle.json --max-age-secs 31536000 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.