Skip to content

Commit 8a90aff

Browse files
sedula27Sneha Edula
andauthored
Move Sign individual challenges to ce-cli (#8)
<img width="569" height="399" alt="image" src="https://github.com/user-attachments/assets/4ee5f8b1-acb1-429b-8730-5dbf9101a171" /> --------- Co-authored-by: Sneha Edula <sedula@credengine.onmicrosoft.com>
1 parent 5e28c0b commit 8a90aff

10 files changed

Lines changed: 1198 additions & 211 deletions

File tree

README.md

Lines changed: 102 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
# Credential Engine CLI (`ce`)
22

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

5+
All commands follow the format:
56

7+
```
8+
ce <noun> [<noun>] <verb> [--<parameters>]
9+
```
610

711
## Requirements
812

913
- Python 3.10+
1014

11-
12-
1315
## Installation
1416

1517
```bash
@@ -18,72 +20,122 @@ pip install -e .
1820

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

21-
2223
## Quick start
2324

2425
```bash
25-
# Sign in
26+
# Pick an environment, then sign in
27+
ce env list
28+
ce env use dev
2629
ce login
27-
30+
ce whoami
2831
```
2932

30-
3133
## Command reference
3234

33-
### Authentication
35+
### Account (`ce`)
3436

35-
| Command | Description |
36-
|---|---|
37-
| `ce login` | Sign in via Keycloak device authorization |
38-
| `ce logout` | Sign out and revoke tokens |
39-
| `ce whoami` | Show who you're signed in as |
37+
| Command | Description |
38+
| ----------- | ----------------------------------------- |
39+
| `ce login` | Sign in via Keycloak device authorization |
40+
| `ce logout` | Sign out and revoke tokens |
41+
| `ce whoami` | Show who you're signed in as |
4042

4143
### Environments (`ce env`)
4244

4345
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).
4446

45-
| Command | Description |
46-
|---|---|
47-
| `ce env list` | List all environments (● marks the active one) |
48-
| `ce env use <n>` | Switch to a different environment |
47+
| Command | Description |
48+
| ------------------- | ---------------------------------------------- |
49+
| `ce env list` | List all environments (● marks the active one) |
50+
| `ce env use <name>` | Switch to a different environment |
4951

5052
```bash
5153
ce env list
5254
ce env use dev
5355
```
5456

57+
### IIR — Issuer Identity Registry (`ce iir`)
58+
59+
IIR commands are grouped by the noun they operate on:
5560

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

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

60-
**Phase 1 — Generate signed challenges:**
70+
#### Sign a single challenge (`ce iir challenge sign`)
71+
72+
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`.
6173

6274
```bash
63-
ce iir bulk-upload-dids sign-challenges --csv input.csv
75+
ce iir challenge sign \
76+
--private-key z3u2en... \
77+
--challenge-file challenge.json \
78+
--output signed-challenge.jwt
6479
```
6580

66-
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.
81+
| Flag | Required | Description |
82+
| ------------------ | -------- | -------------------------------------------------------------------- |
83+
| `--private-key` | Yes | Ed25519 private seed, multibase base58btc (starts with `z`) |
84+
| `--challenge-file` | Yes | Path to the challenge JSON produced by CE |
85+
| `--output`, `-o` | No | Write the JWT to this file. If omitted, the JWT is printed to stdout |
86+
87+
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).
88+
89+
Example challenge file:
90+
91+
```json
92+
{
93+
"did": "did:key:z6Mkh...#z6Mkh...",
94+
"challenge": "abc123-nonce",
95+
"aud": "https://credentialengine.org/iir",
96+
"iat": 1779317055,
97+
"exp": 1779317655
98+
}
99+
```
67100

68-
**Phase 2 — Publish to the IIR registry:**
101+
On success, the JWT is written (or printed) and a verification confirmation is shown on stderr:
102+
103+
```
104+
Signature verification: OK (matches DID public key)
105+
Wrote Proof JWT to: signed-challenge.jwt
106+
```
107+
108+
#### Bulk DID upload
109+
110+
Register DIDs for multiple organizations in one go. The process has two phases.
111+
112+
**Phase 1 — Sign challenges (`ce iir challenge bulk-sign`):**
113+
114+
```bash
115+
ce iir challenge bulk-sign --csv input.csv --output output.csv
116+
```
117+
118+
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`).
119+
120+
**Phase 2 — Publish to the IIR registry (`ce iir issuer-did publish`):**
69121

70122
```bash
71-
ce iir bulk-upload-dids publish --output-file Output-input-20240101T000000Z.csv
123+
ce iir issuer-did publish --input output.csv
72124
```
73125

74-
Verifies the JWT signatures server-side and publishes each issuer to the IIR.
126+
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`).
75127

76128
#### Input CSV format
77129

78-
| Column | Required | Description |
79-
|---|---|---|
80-
| `CTID` | Yes | The organization's CTID (must be published in the registry) |
81-
| `DID` | Yes | `did:key:...` or `did:web:...` |
82-
| `VerificationMethod` | Yes | Full verification method ID (e.g., `did:key:z6Mk...#z6Mk...`) |
83-
| `Algorithm` | For did:key | `Ed25519`, `secp256k1`, `P-256`, or `X25519` |
84-
| `PrivateKey` | Yes | Multibase-encoded private key for signing |
85-
| `ValidFrom` | No | Date when the issuer becomes valid (MM/DD/YYYY) |
86-
| `ValidUntil` | No | Date when the issuer expires (MM/DD/YYYY) |
130+
| Column | Required | Description |
131+
| -------------------- | ----------- | ------------------------------------------------------------- |
132+
| `CTID` | Yes | The organization's CTID (must be published in the registry) |
133+
| `DID` | Yes | `did:key:...` or `did:web:...` |
134+
| `VerificationMethod` | Yes | Full verification method ID (e.g., `did:key:z6Mk...#z6Mk...`) |
135+
| `Algorithm` | For did:key | `Ed25519`, `secp256k1`, `P-256`, or `X25519` |
136+
| `PrivateKey` | Yes | Multibase-encoded private key for signing |
137+
| `ValidFrom` | No | Date when the issuer becomes valid (MM/DD/YYYY) |
138+
| `ValidUntil` | No | Date when the issuer expires (MM/DD/YYYY) |
87139

88140
Example:
89141

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

96148
`ValidFrom` and `ValidUntil` are optional.
97149

98-
99-
100-
101150
## Project structure
102151

103152
```
104153
ce-cli/
105154
├── pyproject.toml
106155
└── ce/
107-
├── main.py # Root CLI group + top-level aliases
156+
├── main.py # Root CLI group
108157
├── auth/
109-
│ ├── device_flow.py # RFC 8628 Keycloak client
110-
│ └── token_manager.py # Silent token refresh + require_login()
158+
│ ├── device_flow.py # RFC 8628 Keycloak client
159+
│ └── token_manager.py # Silent token refresh + require_login()
111160
├── commands/
112-
│ ├── auth.py # ce login / logout / account show
113-
│ ├── env.py # ce env list/add/use/show/remove
114-
│ ├── config.py # ce config set/get/list/reset
115-
│ └── resource.py # ce resource list/show/create/delete
161+
│ ├── auth.py # ce login / logout / whoami
162+
│ ├── env.py # ce env list / use / show / add / remove
163+
│ ├── config.py # ce config set / get / list / reset
164+
│ ├── iir.py # ce iir challenge bulk-sign, ce iir issuer-did publish
165+
│ ├── iir_sign_challenge.py # ce iir challenge sign
166+
│ └── resource.py # ce resource list / show / create / delete
116167
├── config/
117-
│ ├── settings.py # OIDCSettings, token I/O, paths
118-
│ └── context.py # Environment model, active-env helpers
168+
│ ├── settings.py # OIDCSettings, token I/O, paths
169+
│ └── context.py # Environment model, active-env helpers
119170
├── iir/
120-
│ ├── csv_processor.py # Bulk DID upload (sign-challenges + publish)
121-
│ └── did_ops.py # DID validation, challenges, JWT signing
171+
│ ├── csv_processor.py # Bulk DID upload (bulk-sign + publish)
172+
│ └── did_ops.py # DID validation, challenges, JWT signing
122173
└── utils/
123-
├── http.py # Authenticated httpx wrapper + APIError
124-
├── output.py # table/json/yaml/tsv renderer + @output_option
125-
└── errors.py # @handle_api_errors decorator
174+
├── http.py # Authenticated httpx wrapper + APIError
175+
├── output.py # table/json/yaml/tsv renderer + @output_option
176+
└── errors.py # @handle_api_errors decorator
126177
```

ce/commands/auth.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Auth command group: ce login, ce logout, ce account show."""
1+
"""Auth command group: ce account login, ce account logout, ce account show."""
22

33
from __future__ import annotations
44

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

2828

29-
# ce login
29+
# ce account login
3030

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

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

6262
client = KeycloakDeviceFlowClient(env)
@@ -103,7 +103,7 @@ def login(env_name: str | None) -> None:
103103
raise SystemExit(1)
104104
except TokenExpiredError:
105105
console.print()
106-
err_console.print("The device code expired. Run [bold]ce login[/bold] again.")
106+
err_console.print("The device code expired. Run [bold]ce account login[/bold] again.")
107107
raise SystemExit(1)
108108
except DeviceFlowError as exc:
109109
console.print()
@@ -126,11 +126,11 @@ def login(env_name: str | None) -> None:
126126
greeting += f" [dim]({email})[/dim]"
127127
greeting += f" [dim]-> {active_name} ({env.label})[/dim]"
128128
console.print(greeting)
129-
console.print(" Run [bold]ce whoami show[/bold] to view your account details.")
129+
console.print(" Run [bold]ce whoami[/bold] to view your account details.")
130130
console.print()
131131

132132

133-
# ce logout
133+
# ce account logout
134134

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

159159

160-
# ce whoami
160+
# ce account show
161161

162162
@click.command("show")
163163
def account_show() -> None:
@@ -166,7 +166,7 @@ def account_show() -> None:
166166

167167
tokens = load_tokens()
168168
if tokens is None:
169-
console.print("Not signed in. Run [bold]ce login[/bold].")
169+
console.print("Not signed in. Run [bold]ce account login[/bold].")
170170
return
171171

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

178178
token = get_valid_access_token()
179179
if not token:
180-
err_console.print("Session expired. Run [bold]ce login[/bold] again.")
180+
err_console.print("Session expired. Run [bold]ce account login[/bold] again.")
181181
return
182182

183183
client = KeycloakDeviceFlowClient(env)

0 commit comments

Comments
 (0)