aethel-sdk 0.9.1

Ergonomic Rust SDK over aethel-core's post-quantum identity primitives: generate, sign, verify, project, recover
Documentation
# Running an agent identity


The README shows the calls. This guide shows how to run an identity: keep it across restarts,
decide whom to trust, and hand someone a receipt they can check later without calling anyone.

Every Rust block below is compiled as a doctest, and the two examples run in CI, so this guide
cannot drift from the API.

## Persistence


Generate once, seal the identity, store the sealed blob, and open it at startup.

```rust
use aethel_sdk::Identity;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut identity = Identity::generate()?;

    // The sealing key must be 32 bytes of high-entropy key material, NOT a password.
    let sealing_key = [7u8; 32]; // stand-in: load yours from wherever you keep secrets
    let sealed = identity.export_sealed(&sealing_key)?;
    // ... write `sealed` to disk or a database ...

    // At the next startup:
    let restored = Identity::open_sealed(&sealed, &sealing_key)?;
    assert_eq!(restored.public_key(), identity.public_key());
    Ok(())
}
```

**What the sealing key is.** It encrypts and authenticates the stored blob. It is not the
identity's signing key and never leaves your process. It belongs wherever you already keep
secrets for this agent: an OS keystore, a secrets manager, a hardware module. Keep it apart
from the sealed blob, or sealing adds nothing.

**If it is lost.** The blob cannot be opened and the identity is gone. Plan for that before it
happens: `Identity::split_for_recovery` turns the identity into five authenticated shares, any
three of which recover it with `Identity::recover_from_shares`. Give the shares to different
custodians.

**Never** store the unsealed secret, and never have a service hold it for the agent. The SDK
does not expose it: signing happens inside the embedded component.

## Publishing the key


The agent's public identifier is its Multikey string, a W3C Multikey that names the algorithm.

```rust
use aethel_sdk::{public_key_from_multibase, Identity};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let identity = Identity::generate()?;
    let multikey = identity.public_key_multibase();

    // Anyone can decode it and learn the algorithm and key bytes.
    let key = public_key_from_multibase(&multikey)?;
    assert_eq!(key.as_bytes(), identity.public_key());
    Ok(())
}
```

What it proves: that whoever signs with the matching private key controls this identity. What
it does not prove: who that is, or that they are authorised to do anything. A valid signature
from an unknown key means only that someone holds that key.

If you would otherwise reuse this identifier with several unrelated parties, do not. A single
identifier lets them correlate the agent. Use `Identity::project_at` to present a fresh,
context-bound projection to each one instead.

## Trusting a signer


Trust is the verifier's decision. A verifier keeps its own allowlist of Multikey strings and
accepts a signature only when it is valid and its key is on that list. There is no registry
run by us. See `examples/trusted_signer.rs`: two identities, a verifier that trusts one of
them, and a run that fails if the trusted one is refused or the untrusted one is accepted.

```sh
cargo run --example trusted_signer
```

## Offline receipts


An agent signs a statement of what it did, for whom and when. Anyone holding the agent's
public key can verify it later, offline, with no call to 0x307. See
`examples/offline_receipt.rs`. Signing and verifying are separate processes, and verifying
reads only files:

```sh
cargo run --example offline_receipt -- sign   ./receipt
cargo run --example offline_receipt -- verify ./receipt
```

Change one byte of `receipt/statement.txt` and `verify` exits non-zero.

## Signing for a purpose


An agent that signs several kinds of statement with one key needs each signature bound to what
it is for, so a signature made for one cannot be replayed as another. Sign under a registered
purpose:

```rust
use aethel_sdk::{verify, verify_with_purpose, Identity, Purpose};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut identity = Identity::generate()?;
    let login = Purpose::from_name("AUTH_LOGIN_V1")?;
    let step_up = Purpose::from_name("AUTH_STEP_UP_V1")?;

    let message = b"a statement";
    let signature = identity.sign_with_purpose(&login, message)?;

    // It verifies under the purpose it was made for, and nowhere else.
    assert!(verify_with_purpose(identity.public_key(), &login, message, &signature)?);
    assert!(!verify_with_purpose(identity.public_key(), &step_up, message, &signature)?);
    assert!(!verify(identity.public_key(), message, &signature)?);
    Ok(())
}
```

Purposes come from aethel-core's registry, read from the embedded component, so there are no
ad hoc context strings: `Purpose::from_name` refuses a name that is not registered, and there
is no way to build a `Purpose` from bytes. `Purpose::registered()` lists them all. Plain `sign`
and `verify` are the empty context, so a plain signature fails under every purpose. Use one
purpose per kind of statement, and never sign under a purpose other than the one you were asked
for.

Since 0.9.1 the registry has a purpose for agent receipts, `AGENT_RECEIPT_V1`
(`aethel-core/agent-receipt/v1`), from aethel-core 0.7.5. A receipt signed under it verifies
under no other purpose. The offline receipt example above still signs with `sign_typed`, which
is the empty context. To bind a receipt to being a receipt, sign it with
`sign_typed_with_purpose` and `Purpose::from_name("AGENT_RECEIPT_V1")`, and verify it with
`verify_typed_with_purpose`.

## What this does not do yet


- Credentials are experimental and off by default (`experimental-credentials`). Do not rely
  on them.