Skip to content
Merged
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
153 changes: 102 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# Credential Engine CLI (`ce`)

A command-line tool for managing Credential Engine platform resources.
A command-line tool for managing Credential Engine platform resources.

All commands follow the format:

```
ce <noun> [<noun>] <verb> [--<parameters>]
```

## Requirements

- Python 3.10+



## Installation

```bash
Expand All @@ -18,72 +20,122 @@ pip install -e .

This puts the `ce` command on your `$PATH`.


## Quick start

```bash
# Sign in
# Pick an environment, then sign in
ce env list
ce env use dev
ce login

ce whoami
```


## Command reference

### Authentication
### Account (`ce`)

| Command | Description |
|---|---|
| `ce login` | Sign in via Keycloak device authorization |
| `ce logout` | Sign out and revoke tokens |
| `ce whoami` | Show who you're signed in as |
| Command | Description |
| ----------- | ----------------------------------------- |
| `ce login` | Sign in via Keycloak device authorization |
| `ce logout` | Sign out and revoke tokens |
| `ce whoami` | Show who you're signed in as |

### Environments (`ce env`)

Environments let you switch between `dev`, `sandbox`, and `prod` without touching env vars. Each environment maps a friendly name to an API URL (plus optional OIDC overrides).

| Command | Description |
|---|---|
| `ce env list` | List all environments (● marks the active one) |
| `ce env use <n>` | Switch to a different environment |
| Command | Description |
| ------------------- | ---------------------------------------------- |
| `ce env list` | List all environments (● marks the active one) |
| `ce env use <name>` | Switch to a different environment |

```bash
ce env list
ce env use dev
```

### IIR — Issuer Identity Registry (`ce iir`)

IIR commands are grouped by the noun they operate on:

### Bulk DID Upload (`ce iir bulk-upload-dids`)
- `ce iir challenge ...` — sign challenges (single or bulk)
- `ce iir issuer-did ...` — publish signed issuers to the registry

Register DIDs for multiple organizations in one go. The process has two phases:
| Command | Login required | Description |
| ---------------------------- | -------------- | ------------------------------------------------------------ |
| `ce iir challenge sign` | No | Sign a single CE challenge JSON file and produce a Proof JWT |
| `ce iir challenge bulk-sign` | Yes | Bulk-validate and sign challenges from a CSV |
| `ce iir issuer-did publish` | Yes | Publish signed issuers to the IIR registry |

**Phase 1 — Generate signed challenges:**
#### Sign a single challenge (`ce iir challenge sign`)

Signs a CE-generated challenge JSON file with an Ed25519 private key (alg `EdDSA`) and verifies the resulting JWT against the public key resolved from the DID in the payload. No login required — offline for `did:key`, single HTTPS fetch for `did:web`.

```bash
ce iir bulk-upload-dids sign-challenges --csv input.csv
ce iir challenge sign \
--private-key z3u2en... \
--challenge-file challenge.json \
--output signed-challenge.jwt
```

Validates each row (membership, registry lookup, DID resolution), creates challenges, signs JWTs with the provided private keys, and writes the results to an output CSV.
| Flag | Required | Description |
| ------------------ | -------- | -------------------------------------------------------------------- |
| `--private-key` | Yes | Ed25519 private seed, multibase base58btc (starts with `z`) |
| `--challenge-file` | Yes | Path to the challenge JSON produced by CE |
| `--output`, `-o` | No | Write the JWT to this file. If omitted, the JWT is printed to stdout |

The challenge JSON must include `did`, `challenge`, `aud`, `iat`, and `exp`. The `did` field is used as the JWT header `kid` and to resolve the public key for verification (`did:key` resolves offline; `did:web` is fetched over HTTPS using the OS trust store).

Example challenge file:

```json
{
"did": "did:key:z6Mkh...#z6Mkh...",
"challenge": "abc123-nonce",
"aud": "https://credentialengine.org/iir",
"iat": 1779317055,
"exp": 1779317655
}
```

**Phase 2 — Publish to the IIR registry:**
On success, the JWT is written (or printed) and a verification confirmation is shown on stderr:

```
Signature verification: OK (matches DID public key)
Wrote Proof JWT to: signed-challenge.jwt
```

#### Bulk DID upload

Register DIDs for multiple organizations in one go. The process has two phases.

**Phase 1 — Sign challenges (`ce iir challenge bulk-sign`):**

```bash
ce iir challenge bulk-sign --csv input.csv --output output.csv
```

Validates each row (membership, registry lookup, DID resolution), creates challenges, signs JWTs with the provided private keys, and writes the results to an output CSV. If `--output` is omitted, defaults to `Output-<stem>-<UTC>.csv`. Failed rows are written to `Errors-<stem>-<UTC>.csv` (override with `--errors`).

**Phase 2 — Publish to the IIR registry (`ce iir issuer-did publish`):**

```bash
ce iir bulk-upload-dids publish --output-file Output-input-20240101T000000Z.csv
ce iir issuer-did publish --input output.csv
```

Verifies the JWT signatures server-side and publishes each issuer to the IIR.
Verifies the JWT signatures and publishes each issuer to the IIR. Pass `--yes`/`-y` to skip the confirmation prompt. Failed publishes are written to `PublishErrors-<stem>-<UTC>.csv` (override with `--publish-errors`).

#### Input CSV format

| Column | Required | Description |
|---|---|---|
| `CTID` | Yes | The organization's CTID (must be published in the registry) |
| `DID` | Yes | `did:key:...` or `did:web:...` |
| `VerificationMethod` | Yes | Full verification method ID (e.g., `did:key:z6Mk...#z6Mk...`) |
| `Algorithm` | For did:key | `Ed25519`, `secp256k1`, `P-256`, or `X25519` |
| `PrivateKey` | Yes | Multibase-encoded private key for signing |
| `ValidFrom` | No | Date when the issuer becomes valid (MM/DD/YYYY) |
| `ValidUntil` | No | Date when the issuer expires (MM/DD/YYYY) |
| Column | Required | Description |
| -------------------- | ----------- | ------------------------------------------------------------- |
| `CTID` | Yes | The organization's CTID (must be published in the registry) |
| `DID` | Yes | `did:key:...` or `did:web:...` |
| `VerificationMethod` | Yes | Full verification method ID (e.g., `did:key:z6Mk...#z6Mk...`) |
| `Algorithm` | For did:key | `Ed25519`, `secp256k1`, `P-256`, or `X25519` |
| `PrivateKey` | Yes | Multibase-encoded private key for signing |
| `ValidFrom` | No | Date when the issuer becomes valid (MM/DD/YYYY) |
| `ValidUntil` | No | Date when the issuer expires (MM/DD/YYYY) |

Example:

Expand All @@ -95,32 +147,31 @@ ce-87654321-...,did:web:example.com,did:web:example.com#key-1,,z3u2en...,,

`ValidFrom` and `ValidUntil` are optional.




## Project structure

```
ce-cli/
├── pyproject.toml
└── ce/
├── main.py # Root CLI group + top-level aliases
├── main.py # Root CLI group
├── auth/
│ ├── device_flow.py # RFC 8628 Keycloak client
│ └── token_manager.py # Silent token refresh + require_login()
│ ├── device_flow.py # RFC 8628 Keycloak client
│ └── token_manager.py # Silent token refresh + require_login()
├── commands/
│ ├── auth.py # ce login / logout / account show
│ ├── env.py # ce env list/add/use/show/remove
│ ├── config.py # ce config set/get/list/reset
│ └── resource.py # ce resource list/show/create/delete
│ ├── auth.py # ce login / logout / whoami
│ ├── env.py # ce env list / use / show / add / remove
│ ├── config.py # ce config set / get / list / reset
│ ├── iir.py # ce iir challenge bulk-sign, ce iir issuer-did publish
│ ├── iir_sign_challenge.py # ce iir challenge sign
│ └── resource.py # ce resource list / show / create / delete
├── config/
│ ├── settings.py # OIDCSettings, token I/O, paths
│ └── context.py # Environment model, active-env helpers
│ ├── settings.py # OIDCSettings, token I/O, paths
│ └── context.py # Environment model, active-env helpers
├── iir/
│ ├── csv_processor.py # Bulk DID upload (sign-challenges + publish)
│ └── did_ops.py # DID validation, challenges, JWT signing
│ ├── csv_processor.py # Bulk DID upload (bulk-sign + publish)
│ └── did_ops.py # DID validation, challenges, JWT signing
└── utils/
├── http.py # Authenticated httpx wrapper + APIError
├── output.py # table/json/yaml/tsv renderer + @output_option
└── errors.py # @handle_api_errors decorator
├── http.py # Authenticated httpx wrapper + APIError
├── output.py # table/json/yaml/tsv renderer + @output_option
└── errors.py # @handle_api_errors decorator
```
20 changes: 10 additions & 10 deletions ce/commands/auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Auth command group: ce login, ce logout, ce account show."""
"""Auth command group: ce account login, ce account logout, ce account show."""

from __future__ import annotations

Expand Down Expand Up @@ -26,7 +26,7 @@ def auth_group() -> None:
"""Manage Credential Engine account and authentication."""


# ce login
# ce account login

@click.command("login")
@click.option("--env", "env_name", default=None, metavar="NAME",
Expand All @@ -49,14 +49,14 @@ def login(env_name: str | None) -> None:
else:
result = get_current_environment()
if result is None:
err_console.print("No active environment. Run [bold]ce env use <n>[/bold] first.")
err_console.print("No active environment. Run [bold]ce env use <name>[/bold] first.")
raise SystemExit(1)
active_name, env = result

if is_logged_in():
console.print(f"Already signed in to [bold]{active_name}[/bold] "
f"([dim]{env.label}[/dim]). Run [bold]ce account show[/bold] for details.")
console.print(" To switch accounts, run [bold]ce logout[/bold] first.")
console.print(" To switch accounts, run [bold]ce account logout[/bold] first.")
return

client = KeycloakDeviceFlowClient(env)
Expand Down Expand Up @@ -103,7 +103,7 @@ def login(env_name: str | None) -> None:
raise SystemExit(1)
except TokenExpiredError:
console.print()
err_console.print("The device code expired. Run [bold]ce login[/bold] again.")
err_console.print("The device code expired. Run [bold]ce account login[/bold] again.")
raise SystemExit(1)
except DeviceFlowError as exc:
console.print()
Expand All @@ -126,11 +126,11 @@ def login(env_name: str | None) -> None:
greeting += f" [dim]({email})[/dim]"
greeting += f" [dim]-> {active_name} ({env.label})[/dim]"
console.print(greeting)
console.print(" Run [bold]ce whoami show[/bold] to view your account details.")
console.print(" Run [bold]ce whoami[/bold] to view your account details.")
console.print()


# ce logout
# ce account logout

@click.command("logout")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
Expand All @@ -157,7 +157,7 @@ def logout(yes: bool) -> None:
console.print("Signed out successfully.")


# ce whoami
# ce account show

@click.command("show")
def account_show() -> None:
Expand All @@ -166,7 +166,7 @@ def account_show() -> None:

tokens = load_tokens()
if tokens is None:
console.print("Not signed in. Run [bold]ce login[/bold].")
console.print("Not signed in. Run [bold]ce account login[/bold].")
return

result = get_current_environment()
Expand All @@ -177,7 +177,7 @@ def account_show() -> None:

token = get_valid_access_token()
if not token:
err_console.print("Session expired. Run [bold]ce login[/bold] again.")
err_console.print("Session expired. Run [bold]ce account login[/bold] again.")
return

client = KeycloakDeviceFlowClient(env)
Expand Down
Loading
Loading