Skip to content
Closed
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
3 changes: 3 additions & 0 deletions crates/auths-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::commands::error_lookup::ErrorLookupCommand;
use crate::commands::id::IdCommand;
use crate::commands::init::InitCommand;
use crate::commands::ipex::IpexCommand;
use crate::commands::keri_emit::KeriEmitCommand;
use crate::commands::key::KeyCommand;
use crate::commands::key_state::KeyStateCommand;
use crate::commands::learn::LearnCommand;
Expand Down Expand Up @@ -129,6 +130,8 @@ pub enum RootCommand {
KeyState(KeyStateCommand),
#[command(hide = true, name = "did-webs")]
DidWebs(DidWebsCommand),
#[command(hide = true, name = "keri-emit")]
KeriEmit(KeriEmitCommand),
#[command(hide = true, name = "tls-cert")]
TlsCert(TlsCertCommand),
#[command(hide = true)]
Expand Down
128 changes: 128 additions & 0 deletions crates/auths-cli/src/commands/keri_emit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//! `auths keri-emit` — emit a raw KERI event (JSON) from deterministic inputs.
//!
//! A hidden interop surface, like `did-webs` / `key-state`: it takes fixed,
//! caller-supplied inputs (keys, delegator, seals) and prints the canonical event
//! JSON the auths KERI builders produce, so the conformance suite can diff it
//! byte-for-byte against the keripy reference (`eventing.incept(delpre=...)` /
//! `eventing.interact(data=[seal])`). It builds nothing new — it calls the same
//! `auths_keri` finalizers the real delegation path uses.
//!
//! It never touches a KEL or the keychain; it is a pure event serializer.

use anyhow::{Result, anyhow};
use auths_keri::{
CesrKey, DipEvent, DipEventInit, Event, IxnEvent, KeriSequence, Prefix, Said, Seal, Threshold,
VersionString, finalize_dip_event, finalize_ixn_event,
};
use clap::Parser;

use crate::config::CliConfig;

/// Emit a raw KERI event as canonical JSON (interop / conformance surface).
#[derive(Parser, Debug, Clone)]
#[command(
about = "Emit a raw KERI event (dip/ixn) as JSON from deterministic inputs (interop surface)",
after_help = "Examples:
auths keri-emit dip --key DA... --delegator EAbc... [--next EN...]
auths keri-emit ixn --pre EAbc... --sn 1 --prev EPrev... --seal-digest EDev..."
)]
pub struct KeriEmitCommand {
#[command(subcommand)]
pub kind: KeriEmitKind,
}

/// Which event to emit.
#[derive(clap::Subcommand, Debug, Clone)]
pub enum KeriEmitKind {
/// Delegated inception (`dip`): the delegate self-signs; `di` names the delegator.
Dip(DipArgs),
/// Interaction (`ixn`): anchors one seal in the KEL (e.g. a delegator-side revocation).
Ixn(IxnArgs),
}

/// Inputs for a delegated inception.
#[derive(Parser, Debug, Clone)]
pub struct DipArgs {
/// The delegate's current signing key, CESR-encoded (qb64) — the same form keripy's `verfer.qb64`.
#[clap(long, value_name = "CESR")]
pub key: String,
/// The delegator's AID prefix (becomes the dip's `di`).
#[clap(long, value_name = "PREFIX")]
pub delegator: String,
/// Optional next-key commitment (pre-rotation digest). Absent → `nt=0`, `n=[]`.
#[clap(long, value_name = "SAID")]
pub next: Option<String>,
}

/// Inputs for an interaction event.
#[derive(Parser, Debug, Clone)]
pub struct IxnArgs {
/// The AID prefix authoring the interaction (the delegator, for a revocation).
#[clap(long, value_name = "PREFIX")]
pub pre: String,
/// Sequence number of this interaction.
#[clap(long)]
pub sn: u64,
/// Prior event SAID (`p`).
#[clap(long, value_name = "SAID")]
pub prev: String,
/// A digest seal `{d}` to anchor (auths's delegator-side revocation marker: the device prefix).
#[clap(long, value_name = "SAID")]
pub seal_digest: String,
}

impl KeriEmitCommand {
/// Build the requested event, finalize its SAID, and print the canonical JSON.
pub fn execute(&self, _ctx: &CliConfig) -> Result<()> {
let json = match &self.kind {
KeriEmitKind::Dip(args) => emit_dip(args)?,
KeriEmitKind::Ixn(args) => emit_ixn(args)?,
};
println!("{json}");
Ok(())
}
}

/// Build + finalize a delegated inception and return its canonical JSON.
fn emit_dip(args: &DipArgs) -> Result<String> {
let (nt, n) = match &args.next {
Some(next) => (
Threshold::Simple(1),
vec![Said::new_unchecked(next.clone())],
),
None => (Threshold::Simple(0), vec![]),
};
let dip = finalize_dip_event(DipEvent::new(DipEventInit {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(args.key.clone())],
nt,
n,
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
di: Prefix::new_unchecked(args.delegator.clone()),
}))
.map_err(|e| anyhow!("finalize dip: {e}"))?;
serde_json::to_string(&Event::Dip(dip)).map_err(|e| anyhow!("serialize dip: {e}"))
}

/// Build + finalize an interaction anchoring a single digest seal; return its canonical JSON.
fn emit_ixn(args: &IxnArgs) -> Result<String> {
let ixn = finalize_ixn_event(IxnEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::new_unchecked(args.pre.clone()),
s: KeriSequence::new(args.sn as u128),
p: Said::new_unchecked(args.prev.clone()),
a: vec![Seal::Digest {
d: Said::new_unchecked(args.seal_digest.clone()),
}],
})
.map_err(|e| anyhow!("finalize ixn: {e}"))?;
serde_json::to_string(&Event::Ixn(ixn)).map_err(|e| anyhow!("serialize ixn: {e}"))
}
1 change: 1 addition & 0 deletions crates/auths-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub mod id;
pub mod index;
pub mod init;
pub mod ipex;
pub mod keri_emit;
pub mod key;
pub mod key_detect;
pub mod key_state;
Expand Down
1 change: 1 addition & 0 deletions crates/auths-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ fn run() -> Result<()> {
RootCommand::Key(cmd) => cmd.execute(&ctx),
RootCommand::KeyState(cmd) => cmd.execute(&ctx),
RootCommand::DidWebs(cmd) => cmd.execute(&ctx),
RootCommand::KeriEmit(cmd) => cmd.execute(&ctx),
RootCommand::TlsCert(cmd) => cmd.execute(&ctx),
RootCommand::Oobi(cmd) => cmd.execute(&ctx),
RootCommand::Ipex(cmd) => cmd.execute(&ctx),
Expand Down
29 changes: 29 additions & 0 deletions tests/conformance/oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,32 @@ def ipex_admit(sender: str, recipient: str, *, dt: str = ACDC_DT) -> dict:
date=dt,
)
return json.loads(admit.raw.decode())


# ── Surfaces 7 & 8: delegated inception + delegator-side revocation ixn ───────
def dip(delegator_pre: str, key_qb64: str, *, next_said: str | None = None) -> dict:
"""keripy delegated inception (`dip`) for `key_qb64` under `delegator_pre`.

Mirrors auths `keri-emit dip`: `eventing.incept(keys=[key], delpre=delegator,
ndigs=[next] or None, code=Blake3_256)`. The delegated AID (`d == i`) and every
field are keripy's own — a byte match proves auths's delegated AID is exactly
what a keripy verifier would compute for the same inputs.
"""
srdr = eventing.incept(
keys=[key_qb64],
delpre=delegator_pre,
ndigs=[next_said] if next_said else None,
code=MtrDex.Blake3_256,
)
return json.loads(srdr.raw.decode())


def ixn_digest_seal(pre: str, sn: int, prev: str, seal_digest: str) -> dict:
"""keripy interaction (`ixn`) anchoring one digest seal `{"d": seal_digest}`.

Mirrors auths `keri-emit ixn --seal-digest` — auths's delegator-side revocation
marker (a single-author root ixn anchoring the revoked device's prefix as a
digest seal): `eventing.interact(pre, dig=prev, sn=sn, data=[{"d": seal_digest}])`.
"""
srdr = eventing.interact(pre=pre, dig=prev, sn=sn, data=[{"d": seal_digest}])
return json.loads(srdr.raw.decode())
78 changes: 78 additions & 0 deletions tests/conformance/test_keripy_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,81 @@ def test_ipex_admit(run_auths, write_json):
assert canon(out) == canon(load_vector("ipex-admit.json"))
# The prior must thread the grant's SAID (the IPEX loop-closing detail).
assert out["p"] == json.loads(grant_res.stdout)["d"]


# ── Surface 7: delegated inception (dip) — the Workstream A interop claim ─────
def test_dip_delegated_inception(run_auths):
"""auths `keri-emit dip` == keripy delegated inception (byte-for-byte AID).

The interop claim for the device-delegation model: a delegate auths incepts as
a delegated identifier must compute the SAME delegated AID (`d == i`) keripy
would, or a keripy verifier would reject it. Delegate key = the deterministic
ed2 key; delegator = the ed AID.
"""
delegator = o.icp_ed().pre
key = o.ed2_verfer().qb64

out = run_auths(["keri-emit", "dip", "--key", key, "--delegator", delegator]).json
expected = o.dip(delegator, key)

assert canon(out) == canon(expected), (
f"\n auths : {canon(out)}\n keripy: {canon(expected)}"
)
assert out["d"] == out["i"], "a delegated AID is self-addressing (d == i)"
assert out["di"] == delegator, "di names the delegator"


# ── Surface 8: delegator-side revocation ixn ─────────────────────────────────
def test_revocation_ixn_is_keripy_parseable(run_auths):
"""auths `keri-emit ixn --seal-digest` == keripy interact with a digest seal.

auths revokes a lost device delegator-side: a single-author root `ixn`
anchoring the device's prefix as a digest seal. This asserts the EVENT is
byte-identical to keripy's `interact(data=[{"d": ...}])` — i.e. a keripy
verifier can parse/replay it. Whether keripy *interprets* that digest seal as a
device revocation is a separate protocol-semantics question (see the interop
findings write-up) — this surface only proves the wire event conforms.
"""
icp = o.icp_ed()
pre, prev = icp.pre, icp.said
device_prefix = o.dip(pre, o.ed2_verfer().qb64)["i"]

out = run_auths(
[
"keri-emit", "ixn",
"--pre", pre,
"--sn", "1",
"--prev", prev,
"--seal-digest", device_prefix,
]
).json
expected = o.ixn_digest_seal(pre, 1, prev, device_prefix)

assert canon(out) == canon(expected), (
f"\n auths : {canon(out)}\n keripy: {canon(expected)}"
)
assert out["a"] == [{"d": device_prefix}], "the ixn anchors the device-prefix digest seal"


# ── Surface 7b: delegated inception WITH pre-rotation (auths's real dip shape) ─
def test_dip_with_pre_rotation(run_auths):
"""auths dip with a next-key commitment (`nt=1`, `n=[…]`) == keripy.

auths's real delegated inception carries pre-rotation (a next-key digest), so
this proves the *actual* shape — not just the bare form — computes the same
delegated AID keripy would. Any valid CESR digest serves as the commitment
(both sides embed it identically and SAID over it).
"""
delegator = o.icp_ed().pre
key = o.ed2_verfer().qb64
nxt = o.icp_ed().said # a valid CESR Blake3 digest used as the next-key commitment

out = run_auths(
["keri-emit", "dip", "--key", key, "--delegator", delegator, "--next", nxt]
).json
expected = o.dip(delegator, key, next_said=nxt)

assert canon(out) == canon(expected), (
f"\n auths : {canon(out)}\n keripy: {canon(expected)}"
)
assert out["nt"] == "1" and out["n"] == [nxt], "pre-rotation: nt=1, n=[commitment]"
Loading