A library for ATProtocol identities.

refactor: Standardize CLI tools with clap and enhance security

* Add clap dependency as optional feature for all CLI tools
* Convert all 12 CLI binaries to use clap derive patterns for consistent argument parsing
* Implement secure password prompts with rpassword and secrecy crates
* Add comprehensive help documentation and environment variable support
* Update CLAUDE.md with detailed CLI usage examples and patterns
* Feature-gate all binaries behind 'clap' feature to maintain library-first design
* Enhance Dockerfile build process to include clap feature

+94 -10
CLAUDE.md
··· 9 9 ## Common Commands 10 10 11 11 - **Build**: `cargo build` 12 + - **Build with CLI tools**: `cargo build --features clap --bins` 12 13 - **Run tests**: `cargo test` 13 14 - **Run specific test**: `cargo test test_name` 14 15 - **Check code**: `cargo check` 15 16 - **Format code**: `cargo fmt` 16 17 - **Lint**: `cargo clippy` 17 - - **Run CLI tool**: `cargo run --bin atproto-identity-resolve -- <handle_or_did>` 18 - - **Run CLI with DID document**: `cargo run --bin atproto-identity-resolve -- --did-document <handle_or_did>` 18 + 19 + ### CLI Tools (require --features clap) 20 + 21 + All CLI tools now use clap for consistent command-line argument processing. Use `--help` with any tool for detailed usage information. 22 + 23 + #### Identity Management 24 + - **Resolve identities**: `cargo run --features clap --bin atproto-identity-resolve -- <handle_or_did>` 25 + - **Resolve with DID document**: `cargo run --features clap --bin atproto-identity-resolve -- --did-document <handle_or_did>` 26 + - **Generate keys**: `cargo run --features clap --bin atproto-identity-key -- generate p256` 27 + - **Sign data**: `cargo run --features clap --bin atproto-identity-sign -- <did_key> <json_file>` 28 + - **Validate signatures**: `cargo run --features clap --bin atproto-identity-validate -- <did_key> <json_file> <signature>` 29 + 30 + #### Record Operations 31 + - **Sign records**: `cargo run --features clap --bin atproto-record-sign -- <issuer_did> <signing_key> <record_input> repository=<repo> collection=<collection>` 32 + - **Verify records**: `cargo run --features clap --bin atproto-record-verify -- <issuer_did> <key> <record_input> repository=<repo> collection=<collection>` 33 + 34 + #### Client Tools 35 + - **App password auth**: `cargo run --features clap --bin atproto-client-app-password -- <subject> <access_token> <xrpc_path>` 36 + - **Session auth**: `cargo run --features clap --bin atproto-client-auth -- login <identifier> <password>` 37 + - **DPoP client**: `cargo run --features clap --bin atproto-client-dpop -- <subject> <private_dpop_key> <access_token> <xrpc_path>` 38 + 39 + #### Streaming and OAuth 40 + - **Jetstream consumer**: `cargo run --features clap --bin atproto-jetstream-consumer -- <hostname> <zstd_dictionary>` 41 + - **OAuth tool**: `cargo run --features clap --bin atproto-oauth-tool -- login <private_signing_key> <subject>` 42 + - **XRPCS service**: `cargo run --features clap --bin atproto-xrpcs-helloworld` 19 43 20 44 ## Architecture 21 45 22 - A comprehensive Rust library with: 23 - - Modular architecture with 8 core modules (resolve, plc, web, model, validation, config, errors, key) 24 - - Complete CLI tool for identity resolution (`atproto-identity-resolve`) 25 - - Rust edition 2024 with modern async/await patterns 26 - - Comprehensive error handling with structured error types 27 - - Multiple external dependencies for HTTP, DNS, JSON, and cryptographic operations 28 - - Full test coverage with unit tests for all modules 46 + A comprehensive Rust workspace with multiple crates: 47 + - **atproto-identity**: Core identity management with 8 modules (resolve, plc, web, model, validation, config, errors, key) 48 + - **atproto-record**: Record signature operations and validation 49 + - **atproto-client**: HTTP client with OAuth and identity integration 50 + - **atproto-jetstream**: WebSocket event streaming with compression 51 + - **atproto-oauth**: OAuth workflow implementation with storage 52 + - **atproto-oauth-axum**: Axum web framework integration for OAuth 53 + - **atproto-xrpcs**: XRPC service framework 54 + - **atproto-xrpcs-helloworld**: Complete example XRPC service 55 + 56 + Features: 57 + - **12 CLI tools** with consistent clap-based command-line interfaces (optional via `clap` feature) 58 + - **Rust edition 2024** with modern async/await patterns 59 + - **Comprehensive error handling** with structured error types 60 + - **Full test coverage** with unit tests across all modules 61 + - **Modern dependencies** for HTTP, DNS, JSON, cryptographic operations, and WebSocket streaming 29 62 30 63 ## Error Handling 31 64 ··· 65 98 66 99 Async calls should be instrumented using the `.instrument()` that references the `use tracing::Instrument;` trait. 67 100 101 + ## Command-Line Interface Pattern 102 + 103 + All CLI tools use the clap library with derive macros for consistent command-line argument processing: 104 + 105 + - **Optional dependency**: clap is an optional dependency in each crate via `clap = { workspace = true, optional = true }` 106 + - **Feature-gated**: All binaries require the `clap` feature via `required-features = ["clap"]` 107 + - **Derive pattern**: Use `#[derive(Parser)]` and `#[derive(Subcommand)]` for command structures 108 + - **Comprehensive help**: Include detailed help documentation in clap attributes 109 + - **Consistent structure**: Use structured arguments with proper types rather than manual parsing 110 + 111 + Example command structure: 112 + ```rust 113 + #[derive(Parser)] 114 + #[command( 115 + name = "tool-name", 116 + version, 117 + about = "Brief description", 118 + long_about = "Detailed description with examples" 119 + )] 120 + struct Args { 121 + /// Argument description 122 + positional_arg: String, 123 + 124 + /// Optional flag description 125 + #[arg(long)] 126 + optional_flag: bool, 127 + } 128 + ``` 129 + 68 130 ## Module Structure 69 131 132 + ### Core Library Modules (atproto-identity) 70 133 - **`src/lib.rs`**: Main library exports 71 134 - **`src/resolve.rs`**: Core resolution logic for handles and DIDs, DNS/HTTP resolution 72 135 - **`src/plc.rs`**: PLC directory client for did:plc resolution ··· 76 139 - **`src/config.rs`**: Configuration management and environment variable handling 77 140 - **`src/errors.rs`**: Structured error types following project conventions 78 141 - **`src/key.rs`**: Cryptographic key operations including signature validation and key identification for P-256 and K-256 curves 79 - - **`src/bin/atproto-identity-resolve.rs`**: CLI tool for identity resolution 142 + 143 + ### CLI Tools (require --features clap) 144 + 145 + #### Identity Management (atproto-identity) 146 + - **`src/bin/atproto-identity-resolve.rs`**: Resolve AT Protocol handles and DIDs to canonical identifiers 147 + - **`src/bin/atproto-identity-key.rs`**: Generate and manage cryptographic keys (P-256, K-256) 148 + - **`src/bin/atproto-identity-sign.rs`**: Create cryptographic signatures of JSON data 149 + - **`src/bin/atproto-identity-validate.rs`**: Validate cryptographic signatures 150 + 151 + #### Record Operations (atproto-record) 152 + - **`src/bin/atproto-record-sign.rs`**: Sign AT Protocol records with cryptographic signatures 153 + - **`src/bin/atproto-record-verify.rs`**: Verify AT Protocol record signatures 154 + 155 + #### Client Tools (atproto-client) 156 + - **`src/bin/atproto-client-app-password.rs`**: Make XRPC calls using app password authentication 157 + - **`src/bin/atproto-client-auth.rs`**: Create and refresh authentication sessions 158 + - **`src/bin/atproto-client-dpop.rs`**: Make XRPC calls using DPoP (Demonstration of Proof-of-Possession) authentication 159 + 160 + #### Streaming and Services 161 + - **`crates/atproto-jetstream/src/bin/atproto-jetstream-consumer.rs`**: Consume AT Protocol events via WebSocket streaming 162 + - **`crates/atproto-oauth-axum/src/bin/atproto-oauth-tool.rs`**: OAuth authentication workflow management 163 + - **`crates/atproto-xrpcs-helloworld/src/main.rs`**: Complete example XRPC service with DID web functionality 80 164 81 165 ## Documentation 82 166
+161
Cargo.lock
··· 33 33 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 34 34 35 35 [[package]] 36 + name = "anstream" 37 + version = "0.6.19" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 40 + dependencies = [ 41 + "anstyle", 42 + "anstyle-parse", 43 + "anstyle-query", 44 + "anstyle-wincon", 45 + "colorchoice", 46 + "is_terminal_polyfill", 47 + "utf8parse", 48 + ] 49 + 50 + [[package]] 51 + name = "anstyle" 52 + version = "1.0.11" 53 + source = "registry+https://github.com/rust-lang/crates.io-index" 54 + checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 55 + 56 + [[package]] 57 + name = "anstyle-parse" 58 + version = "0.2.7" 59 + source = "registry+https://github.com/rust-lang/crates.io-index" 60 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 61 + dependencies = [ 62 + "utf8parse", 63 + ] 64 + 65 + [[package]] 66 + name = "anstyle-query" 67 + version = "1.1.3" 68 + source = "registry+https://github.com/rust-lang/crates.io-index" 69 + checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 70 + dependencies = [ 71 + "windows-sys 0.59.0", 72 + ] 73 + 74 + [[package]] 75 + name = "anstyle-wincon" 76 + version = "3.0.9" 77 + source = "registry+https://github.com/rust-lang/crates.io-index" 78 + checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 79 + dependencies = [ 80 + "anstyle", 81 + "once_cell_polyfill", 82 + "windows-sys 0.59.0", 83 + ] 84 + 85 + [[package]] 36 86 name = "anyhow" 37 87 version = "1.0.98" 38 88 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 64 114 "atproto-oauth", 65 115 "atproto-record", 66 116 "bytes", 117 + "clap", 67 118 "reqwest", 68 119 "reqwest-chain", 69 120 "reqwest-middleware", 121 + "rpassword", 122 + "secrecy", 70 123 "serde", 71 124 "serde_json", 72 125 "thiserror 2.0.12", ··· 82 135 "anyhow", 83 136 "async-trait", 84 137 "axum", 138 + "clap", 85 139 "ecdsa", 86 140 "elliptic-curve", 87 141 "hickory-resolver", ··· 107 161 "anyhow", 108 162 "async-trait", 109 163 "atproto-identity", 164 + "clap", 110 165 "futures", 111 166 "http", 112 167 "serde", ··· 163 218 "atproto-record", 164 219 "axum", 165 220 "chrono", 221 + "clap", 166 222 "elliptic-curve", 167 223 "hickory-resolver", 168 224 "http", ··· 170 226 "reqwest", 171 227 "reqwest-chain", 172 228 "reqwest-middleware", 229 + "rpassword", 230 + "secrecy", 173 231 "serde", 174 232 "serde_json", 175 233 "thiserror 2.0.12", ··· 185 243 "anyhow", 186 244 "atproto-identity", 187 245 "chrono", 246 + "clap", 188 247 "ecdsa", 189 248 "k256", 190 249 "multibase", ··· 236 295 "atproto-xrpcs", 237 296 "axum", 238 297 "chrono", 298 + "clap", 239 299 "elliptic-curve", 240 300 "hickory-resolver", 241 301 "http", ··· 445 505 ] 446 506 447 507 [[package]] 508 + name = "clap" 509 + version = "4.5.40" 510 + source = "registry+https://github.com/rust-lang/crates.io-index" 511 + checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" 512 + dependencies = [ 513 + "clap_builder", 514 + "clap_derive", 515 + ] 516 + 517 + [[package]] 518 + name = "clap_builder" 519 + version = "4.5.40" 520 + source = "registry+https://github.com/rust-lang/crates.io-index" 521 + checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" 522 + dependencies = [ 523 + "anstream", 524 + "anstyle", 525 + "clap_lex", 526 + "strsim", 527 + ] 528 + 529 + [[package]] 530 + name = "clap_derive" 531 + version = "4.5.40" 532 + source = "registry+https://github.com/rust-lang/crates.io-index" 533 + checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" 534 + dependencies = [ 535 + "heck", 536 + "proc-macro2", 537 + "quote", 538 + "syn", 539 + ] 540 + 541 + [[package]] 542 + name = "clap_lex" 543 + version = "0.7.5" 544 + source = "registry+https://github.com/rust-lang/crates.io-index" 545 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 546 + 547 + [[package]] 548 + name = "colorchoice" 549 + version = "1.0.4" 550 + source = "registry+https://github.com/rust-lang/crates.io-index" 551 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 552 + 553 + [[package]] 448 554 name = "const-oid" 449 555 version = "0.9.6" 450 556 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1265 1371 ] 1266 1372 1267 1373 [[package]] 1374 + name = "is_terminal_polyfill" 1375 + version = "1.70.1" 1376 + source = "registry+https://github.com/rust-lang/crates.io-index" 1377 + checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 1378 + 1379 + [[package]] 1268 1380 name = "itoa" 1269 1381 version = "1.0.15" 1270 1382 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1524 1636 "critical-section", 1525 1637 "portable-atomic", 1526 1638 ] 1639 + 1640 + [[package]] 1641 + name = "once_cell_polyfill" 1642 + version = "1.70.1" 1643 + source = "registry+https://github.com/rust-lang/crates.io-index" 1644 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 1527 1645 1528 1646 [[package]] 1529 1647 name = "openssl" ··· 1985 2103 ] 1986 2104 1987 2105 [[package]] 2106 + name = "rpassword" 2107 + version = "7.4.0" 2108 + source = "registry+https://github.com/rust-lang/crates.io-index" 2109 + checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" 2110 + dependencies = [ 2111 + "libc", 2112 + "rtoolbox", 2113 + "windows-sys 0.59.0", 2114 + ] 2115 + 2116 + [[package]] 2117 + name = "rtoolbox" 2118 + version = "0.0.3" 2119 + source = "registry+https://github.com/rust-lang/crates.io-index" 2120 + checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" 2121 + dependencies = [ 2122 + "libc", 2123 + "windows-sys 0.52.0", 2124 + ] 2125 + 2126 + [[package]] 1988 2127 name = "rustc-demangle" 1989 2128 version = "0.1.24" 1990 2129 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2110 2249 "pkcs8", 2111 2250 "serdect", 2112 2251 "subtle", 2252 + "zeroize", 2253 + ] 2254 + 2255 + [[package]] 2256 + name = "secrecy" 2257 + version = "0.10.3" 2258 + source = "registry+https://github.com/rust-lang/crates.io-index" 2259 + checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" 2260 + dependencies = [ 2261 + "serde", 2113 2262 "zeroize", 2114 2263 ] 2115 2264 ··· 2331 2480 version = "1.2.0" 2332 2481 source = "registry+https://github.com/rust-lang/crates.io-index" 2333 2482 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 2483 + 2484 + [[package]] 2485 + name = "strsim" 2486 + version = "0.11.1" 2487 + source = "registry+https://github.com/rust-lang/crates.io-index" 2488 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 2334 2489 2335 2490 [[package]] 2336 2491 name = "subtle" ··· 2744 2899 version = "1.0.4" 2745 2900 source = "registry+https://github.com/rust-lang/crates.io-index" 2746 2901 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2902 + 2903 + [[package]] 2904 + name = "utf8parse" 2905 + version = "0.2.2" 2906 + source = "registry+https://github.com/rust-lang/crates.io-index" 2907 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2747 2908 2748 2909 [[package]] 2749 2910 name = "uuid"
+3
Cargo.toml
··· 32 32 async-trait = "0.1.88" 33 33 base64 = "0.22.1" 34 34 chrono = {version = "0.4.41", default-features = false, features = ["std", "now"]} 35 + clap = { version = "4.5", features = ["derive", "env"] } 35 36 ecdsa = { version = "0.16.9", features = ["std"] } 36 37 elliptic-curve = { version = "0.13.8", features = ["jwk", "serde"] } 37 38 futures = "0.3" ··· 45 46 reqwest = { version = "0.12", features = ["json", "rustls-tls"] } 46 47 reqwest-chain = "1.0.0" 47 48 reqwest-middleware = { version = "0.4.2", features = ["json", "multipart"]} 49 + rpassword = "7.3" 50 + secrecy = { version = "0.10", features = ["serde"] } 48 51 serde = { version = "1.0", features = ["derive"] } 49 52 serde_ipld_dagcbor = "0.6.3" 50 53 serde_json = "1.0"
+1 -1
Dockerfile
··· 23 23 # - atproto-oauth-axum: 1 binary (oauth-tool) 24 24 # - atproto-client: 1 binary (client-dpop) 25 25 # - atproto-xrpcs-helloworld: 1 binary (xrpcs-helloworld) 26 - RUN cargo build --release --bins 26 + RUN cargo build --release --bins -F clap 27 27 28 28 # Runtime stage - use distroless for minimal attack surface 29 29 FROM gcr.io/distroless/cc-debian12
+27
crates/atproto-client/Cargo.toml
··· 14 14 keywords.workspace = true 15 15 categories.workspace = true 16 16 17 + [[bin]] 18 + name = "atproto-client-app-password" 19 + test = false 20 + bench = false 21 + doc = true 22 + required-features = ["clap"] 23 + 24 + [[bin]] 25 + name = "atproto-client-auth" 26 + test = false 27 + bench = false 28 + doc = true 29 + required-features = ["clap"] 30 + 31 + [[bin]] 32 + name = "atproto-client-dpop" 33 + test = false 34 + bench = false 35 + doc = true 36 + required-features = ["clap"] 37 + 17 38 [dependencies] 18 39 atproto-identity.workspace = true 19 40 atproto-record.workspace = true ··· 30 51 tracing.workspace = true 31 52 urlencoding = "2.1.3" 32 53 bytes = "1.10.1" 54 + clap = { workspace = true, optional = true } 55 + rpassword = { workspace = true, optional = true } 56 + secrecy = { workspace = true, optional = true } 57 + 58 + [features] 59 + clap = ["dep:clap", "dep:rpassword", "dep:secrecy"] 33 60 34 61 [lints] 35 62 workspace = true
+76 -48
crates/atproto-client/src/bin/atproto-client-app-password.rs
··· 13 13 resolve::{create_resolver, resolve_subject}, 14 14 web, 15 15 }; 16 + use clap::Parser; 16 17 use reqwest::header::HeaderMap; 17 - use std::{collections::HashMap, env}; 18 + use rpassword::read_password; 19 + use secrecy::{ExposeSecret, SecretString}; 20 + use std::{collections::HashMap, io::{self, Write}}; 18 21 19 - fn print_usage() { 20 - println!("AT Protocol Client App Password Tool"); 21 - println!(); 22 - println!("Usage:"); 23 - println!(" atproto-client-app-password <subject> <access_token> <xrpc_path> [args...]"); 24 - println!(); 25 - println!("Arguments:"); 26 - println!(" subject Subject identifier to resolve"); 27 - println!(" access_token App password JWT access token"); 28 - println!(" xrpc_path XRPC path with optional prefix:"); 29 - println!(" - query:<path> for GET requests (default)"); 30 - println!(" - procedure:<path> for POST requests"); 31 - println!(" - <path> defaults to GET request"); 32 - println!( 33 - " key=value Additional query parameters (for GET requests) 34 - header=name=value Additional HTTP headers 35 - <file_path> JSON file path (required for procedure: prefix)" 36 - ); 37 - println!(); 38 - println!("Examples:"); 39 - println!(" # GET request (default behavior without prefix)"); 40 - println!( 41 - " atproto-client-app-password alice.bsky.social eyJ0... com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post" 42 - ); 43 - println!(" # GET request (explicit query: prefix)"); 44 - println!( 45 - " atproto-client-app-password alice.bsky.social eyJ0... query:com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post" 46 - ); 47 - println!(" # POST request (requires procedure: prefix and JSON file)"); 48 - println!( 49 - " atproto-client-app-password alice.bsky.social eyJ0... procedure:com.atproto.repo.createRecord data.json" 50 - ); 22 + /// AT Protocol Client App Password Tool 23 + #[derive(Parser)] 24 + #[command( 25 + name = "atproto-client-app-password", 26 + version, 27 + about = "Make authenticated XRPC calls using AT Protocol app password authentication", 28 + long_about = " 29 + A command-line tool for making authenticated XRPC calls using app password 30 + authentication. Resolves subjects to DID documents and constructs authenticated 31 + requests to AT Protocol services. 32 + 33 + XRPC PATH FORMATS: 34 + query:<path> For GET requests (default if no prefix) 35 + procedure:<path> For POST requests (requires JSON file) 36 + <path> Defaults to GET request 37 + 38 + ADDITIONAL ARGUMENTS: 39 + key=value Query parameters (for GET requests) 40 + header=name=value Additional HTTP headers 41 + <file_path> JSON file path (required for procedure calls) 42 + 43 + EXAMPLES: 44 + # GET request: 45 + atproto-client-app-password alice.bsky.social eyJ0... \\ 46 + com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post 47 + 48 + # POST request: 49 + atproto-client-app-password alice.bsky.social eyJ0... \\ 50 + procedure:com.atproto.repo.createRecord data.json 51 + 52 + ENVIRONMENT VARIABLES: 53 + PLC_HOSTNAME PLC directory hostname (default: plc.directory) 54 + USER_AGENT HTTP user agent string (default: auto-generated) 55 + CERTIFICATE_BUNDLES Additional CA certificate bundles 56 + DNS_NAMESERVERS Custom DNS nameserver addresses 57 + " 58 + )] 59 + struct Args { 60 + /// Subject identifier to resolve (e.g., alice.bsky.social) 61 + subject: String, 62 + 63 + /// App password JWT access token from environment variable 64 + #[arg(long, env = "ATPROTO_ACCESS_TOKEN")] 65 + access_token: Option<String>, 66 + 67 + /// XRPC path with optional prefix (query:/procedure:) 68 + xrpc_path: String, 69 + 70 + /// Additional arguments for query parameters, headers, and JSON file paths 71 + additional_args: Vec<String>, 51 72 } 52 73 53 74 #[tokio::main] 54 75 async fn main() -> Result<()> { 55 - // Parse command line arguments 56 - let args: Vec<String> = env::args().skip(1).collect(); 57 - 58 - if args.len() < 3 || args.iter().any(|arg| arg == "--help" || arg == "-h") { 59 - print_usage(); 60 - return Ok(()); 61 - } 76 + let args = Args::parse(); 62 77 63 - let subject = &args[0]; 64 - let access_token = &args[1]; 65 - let xrpc_path_with_prefix = &args[2]; 78 + let subject = &args.subject; 79 + 80 + // Secure access token handling: prefer environment variable, then secure prompt 81 + let access_token = if let Some(env_token) = args.access_token { 82 + SecretString::new(env_token.into()) 83 + } else { 84 + // Use secure prompt with hidden input 85 + print!("Enter access token: "); 86 + io::stdout().flush()?; 87 + let token = read_password()?; 88 + if token.is_empty() { 89 + anyhow::bail!("Access token cannot be empty"); 90 + } 91 + SecretString::new(token.into()) 92 + }; 93 + let xrpc_path_with_prefix = &args.xrpc_path; 66 94 67 95 // Parse the xrpc_path prefix (optional, defaults to query:) 68 96 let (is_procedure, xrpc_path) = if let Some(path) = xrpc_path_with_prefix.strip_prefix("query:") ··· 79 107 let mut query_params = HashMap::new(); 80 108 let mut header_params = HashMap::new(); 81 109 let mut json_data: Option<serde_json::Value> = None; 82 - let mut arg_index = 3; 110 + let mut arg_index = 0; 83 111 84 112 // For procedure calls, expect the next argument to be a file path 85 113 if is_procedure { 86 - if arg_index >= args.len() { 114 + if arg_index >= args.additional_args.len() { 87 115 anyhow::bail!("procedure: prefix requires a JSON file path as the next argument"); 88 116 } 89 - let file_path = &args[arg_index]; 117 + let file_path = &args.additional_args[arg_index]; 90 118 let file_content = std::fs::read_to_string(file_path) 91 119 .map_err(|e| anyhow::anyhow!("Failed to read file '{}': {}", file_path, e))?; 92 120 json_data = Some(serde_json::from_str(&file_content).map_err(|e| { ··· 96 124 } 97 125 98 126 // Parse remaining key=value arguments and header=name=value arguments 99 - for arg in &args[arg_index..] { 127 + for arg in &args.additional_args[arg_index..] { 100 128 if let Some((key, value)) = arg.split_once('=') { 101 129 if key == "header" { 102 130 // Parse header=name=value format ··· 206 234 207 235 // Create app password auth 208 236 let app_auth = AppPasswordAuth { 209 - access_token: access_token.to_string(), 237 + access_token: access_token.expose_secret().to_string(), 210 238 }; 211 239 212 240 // Create HeaderMap from header parameters
+79 -76
crates/atproto-client/src/bin/atproto-client-auth.rs
··· 10 10 resolve::{create_resolver, resolve_subject}, 11 11 web, 12 12 }; 13 + use clap::{Parser, Subcommand}; 14 + use rpassword::read_password; 15 + use secrecy::{ExposeSecret, SecretString}; 13 16 use serde_json::json; 14 - use std::env; 17 + use std::{env, io::{self, Write}}; 15 18 16 19 // Import from public module 17 20 use atproto_client::com::atproto::server::{create_session, refresh_session}; 18 21 19 - fn print_usage() { 20 - println!("AT Protocol Client Authentication Tool"); 21 - println!(); 22 - println!("Usage:"); 23 - println!(" atproto-client-auth <command> [args...]"); 24 - println!(); 25 - println!("Commands:"); 26 - println!(" login <identifier> <password> [auth_factor_token]"); 27 - println!(" Create a new app-password session"); 28 - println!(" identifier: Handle or email for authentication"); 29 - println!(" password: App password or account password"); 30 - println!(" auth_factor_token: Optional 2FA token"); 31 - println!(); 32 - println!(" refresh <refresh_token>"); 33 - println!(" Refresh an existing app-password session"); 34 - println!(" refresh_token: JWT refresh token from previous session"); 35 - println!(); 36 - println!("Environment Variables:"); 37 - println!(" CERTIFICATE_BUNDLES: Custom CA certificate bundles"); 38 - println!(" USER_AGENT: Custom user agent string"); 39 - println!(" DNS_NAMESERVERS: Custom DNS nameservers"); 40 - println!(" PDS_ENDPOINT: Override PDS endpoint (skips DID resolution)"); 41 - println!(); 42 - println!("Examples:"); 43 - println!(" # Login with handle and app password"); 44 - println!(" atproto-client-auth login alice.bsky.social app-password-here"); 45 - println!(); 46 - println!(" # Login with email and 2FA"); 47 - println!(" atproto-client-auth login alice@example.com password123 123456"); 48 - println!(); 49 - println!(" # Refresh session"); 50 - println!(" atproto-client-auth refresh eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9..."); 22 + /// AT Protocol Client Authentication Tool 23 + #[derive(Parser)] 24 + #[command( 25 + name = "atproto-client-auth", 26 + version, 27 + about = "Create and refresh AT Protocol app password authentication sessions", 28 + long_about = " 29 + A command-line tool for managing AT Protocol app password authentication sessions. 30 + Provides commands for creating new sessions and refreshing existing ones. 31 + 32 + ENVIRONMENT VARIABLES: 33 + CERTIFICATE_BUNDLES Custom CA certificate bundles 34 + USER_AGENT Custom user agent string 35 + DNS_NAMESERVERS Custom DNS nameservers 36 + PDS_ENDPOINT Override PDS endpoint (skips DID resolution) 37 + 38 + EXAMPLES: 39 + # Login with handle and app password: 40 + atproto-client-auth login alice.bsky.social app-password-here 41 + 42 + # Login with email and 2FA: 43 + atproto-client-auth login alice@example.com password123 123456 44 + 45 + # Refresh session: 46 + atproto-client-auth refresh eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9... 47 + " 48 + )] 49 + struct Args { 50 + #[command(subcommand)] 51 + command: Commands, 52 + } 53 + 54 + #[derive(Subcommand)] 55 + enum Commands { 56 + /// Create a new app-password session 57 + Login { 58 + /// Handle or email for authentication 59 + identifier: String, 60 + /// App password or account password from environment variable 61 + #[arg(long, env = "ATPROTO_PASSWORD")] 62 + password: Option<String>, 63 + /// Optional 2FA token 64 + auth_factor_token: Option<String>, 65 + }, 66 + /// Refresh an existing app-password session 67 + Refresh { 68 + /// JWT refresh token from previous session 69 + refresh_token: String, 70 + }, 51 71 } 52 72 53 73 #[tokio::main] 54 74 async fn main() -> Result<()> { 55 - // Parse command line arguments 56 - let args: Vec<String> = env::args().skip(1).collect(); 57 - 58 - if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") { 59 - print_usage(); 60 - return Ok(()); 61 - } 62 - 63 - let command = &args[0]; 75 + let args = Args::parse(); 64 76 65 77 // Set up HTTP client configuration 66 78 let certificate_bundles: CertificateBundles = optional_env("CERTIFICATE_BUNDLES").try_into()?; ··· 84 96 85 97 let dns_resolver = create_resolver(dns_nameservers.as_ref()); 86 98 87 - match command.as_str() { 88 - "login" => { 89 - if args.len() < 3 { 90 - eprintln!("Error: login command requires identifier and password"); 91 - eprintln!(); 92 - print_usage(); 93 - return Ok(()); 94 - } 95 - 96 - let identifier = &args[1]; 97 - let password = &args[2]; 98 - let auth_factor_token = args.get(3).map(|s| s.as_str()); 99 - 99 + match args.command { 100 + Commands::Login { 101 + identifier, 102 + password, 103 + auth_factor_token, 104 + } => { 105 + // Secure password handling: prefer environment variable, then secure prompt 106 + let password = if let Some(env_password) = password { 107 + SecretString::new(env_password.into()) 108 + } else { 109 + // Use secure prompt with hidden input 110 + print!("Enter password: "); 111 + io::stdout().flush()?; 112 + let password = read_password()?; 113 + if password.is_empty() { 114 + anyhow::bail!("Password cannot be empty"); 115 + } 116 + SecretString::new(password.into()) 117 + }; 100 118 println!("Creating app password session"); 101 119 println!("Identifier: {}", identifier); 102 120 ··· 108 126 println!("Resolving identifier to find PDS endpoint..."); 109 127 110 128 // Resolve the identifier to a DID 111 - let did = resolve_subject(&http_client, &dns_resolver, identifier).await?; 129 + let did = resolve_subject(&http_client, &dns_resolver, &identifier).await?; 112 130 println!("Resolved DID: {}", did); 113 131 114 132 // Get the DID document based on DID type ··· 137 155 let session = create_session( 138 156 &http_client, 139 157 &pds_endpoint, 140 - identifier, 141 - password, 142 - auth_factor_token, 158 + &identifier, 159 + password.expose_secret(), 160 + auth_factor_token.as_deref(), 143 161 ) 144 162 .await?; 145 163 ··· 167 185 ); 168 186 } 169 187 170 - "refresh" => { 171 - if args.len() < 2 { 172 - eprintln!("Error: refresh command requires refresh_token"); 173 - eprintln!(); 174 - print_usage(); 175 - return Ok(()); 176 - } 177 - 178 - let refresh_token = &args[1]; 179 - 188 + Commands::Refresh { refresh_token } => { 180 189 println!("Refreshing app password session"); 181 190 182 191 // Determine PDS endpoint ··· 193 202 // Refresh session 194 203 println!("Refreshing session..."); 195 204 let refreshed_session = 196 - refresh_session(&http_client, &pds_endpoint, refresh_token).await?; 205 + refresh_session(&http_client, &pds_endpoint, &refresh_token).await?; 197 206 198 207 println!("Session refreshed successfully!"); 199 208 println!(); ··· 223 232 "status": refreshed_session.status 224 233 }))? 225 234 ); 226 - } 227 - 228 - _ => { 229 - eprintln!("Error: Unknown command '{}'", command); 230 - eprintln!(); 231 - print_usage(); 232 235 } 233 236 } 234 237
+90 -53
crates/atproto-client/src/bin/atproto-client-dpop.rs
··· 12 12 resolve::{create_resolver, resolve_subject}, 13 13 web, 14 14 }; 15 + use clap::Parser; 15 16 use reqwest::header::HeaderMap; 16 - use std::{collections::HashMap, env}; 17 + use rpassword::read_password; 18 + use secrecy::{ExposeSecret, SecretString}; 19 + use std::{collections::HashMap, env, io::{self, Write}}; 17 20 18 - fn print_usage() { 19 - println!("AT Protocol Client DPoP Tool"); 20 - println!(); 21 - println!("Usage:"); 22 - println!( 23 - " atproto-client-dpop <subject> <private_dpop_key> <access_token> <xrpc_path> [args...]" 24 - ); 25 - println!(); 26 - println!("Arguments:"); 27 - println!(" subject Subject identifier to resolve"); 28 - println!(" private_dpop_key Private DPoP key for authentication"); 29 - println!(" access_token OAuth access token"); 30 - println!(" xrpc_path XRPC path with optional prefix:"); 31 - println!(" - query:<path> for GET requests (default)"); 32 - println!(" - procedure:<path> for POST requests"); 33 - println!(" - <path> defaults to GET request"); 34 - println!( 35 - " key=value Additional query parameters (for GET requests) 36 - header=name=value Additional HTTP headers 37 - <file_path> JSON file path (required for procedure: prefix)" 38 - ); 39 - println!(); 40 - println!("Examples:"); 41 - println!(" # GET request (default behavior without prefix)"); 42 - println!( 43 - " atproto-client-dpop alice.bsky.social dpop.pem token123 com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post" 44 - ); 45 - println!(" # GET request (explicit query: prefix)"); 46 - println!( 47 - " atproto-client-dpop alice.bsky.social dpop.pem token123 query:com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post" 48 - ); 49 - println!(" # POST request (requires procedure: prefix and JSON file)"); 50 - println!( 51 - " atproto-client-dpop alice.bsky.social dpop.pem token123 procedure:com.atproto.repo.createRecord data.json" 52 - ); 21 + /// AT Protocol Client DPoP Tool 22 + #[derive(Parser)] 23 + #[command( 24 + name = "atproto-client-dpop", 25 + version, 26 + about = "Make authenticated XRPC calls using AT Protocol DPoP authentication", 27 + long_about = " 28 + A command-line tool for making authenticated XRPC calls using DPoP (Demonstration 29 + of Proof-of-Possession) authentication. Resolves subjects to DID documents and 30 + constructs authenticated requests to AT Protocol services. 31 + 32 + XRPC PATH FORMATS: 33 + query:<path> For GET requests (default if no prefix) 34 + procedure:<path> For POST requests (requires JSON file) 35 + <path> Defaults to GET request 36 + 37 + ADDITIONAL ARGUMENTS: 38 + key=value Query parameters (for GET requests) 39 + header=name=value Additional HTTP headers 40 + <file_path> JSON file path (required for procedure calls) 41 + 42 + EXAMPLES: 43 + # GET request: 44 + atproto-client-dpop alice.bsky.social dpop.pem token123 \\ 45 + com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post 46 + 47 + # POST request: 48 + atproto-client-dpop alice.bsky.social dpop.pem token123 \\ 49 + procedure:com.atproto.repo.createRecord data.json 50 + 51 + ENVIRONMENT VARIABLES: 52 + PLC_HOSTNAME PLC directory hostname (default: plc.directory) 53 + USER_AGENT HTTP user agent string (default: auto-generated) 54 + CERTIFICATE_BUNDLES Additional CA certificate bundles 55 + DNS_NAMESERVERS Custom DNS nameserver addresses 56 + " 57 + )] 58 + struct Args { 59 + /// Subject identifier to resolve (e.g., alice.bsky.social) 60 + subject: String, 61 + 62 + /// OAuth access token from environment variable 63 + #[arg(long, env = "ATPROTO_ACCESS_TOKEN")] 64 + access_token: Option<String>, 65 + 66 + /// XRPC path with optional prefix (query:/procedure:) 67 + xrpc_path: String, 68 + 69 + /// Additional arguments for query parameters, headers, and JSON file paths 70 + additional_args: Vec<String>, 53 71 } 54 72 55 73 #[tokio::main] 56 74 async fn main() -> Result<()> { 57 - // Parse command line arguments 58 - let args: Vec<String> = env::args().skip(1).collect(); 75 + let args = Args::parse(); 59 76 60 - if args.len() < 4 || args.iter().any(|arg| arg == "--help" || arg == "-h") { 61 - print_usage(); 62 - return Ok(()); 63 - } 64 - 65 - let subject = &args[0]; 66 - let private_dpop_key = &args[1]; 67 - let access_token = &args[2]; 68 - let xrpc_path_with_prefix = &args[3]; 77 + let subject = &args.subject; 78 + 79 + // Secure DPoP key handling: from environment variable or secure prompt 80 + let private_dpop_key = if let Ok(key_content) = env::var("ATPROTO_DPOP_KEY") { 81 + key_content 82 + } else { 83 + print!("Enter DPoP private key content: "); 84 + io::stdout().flush()?; 85 + let key_content = read_password()?; 86 + if key_content.is_empty() { 87 + anyhow::bail!("DPoP key content cannot be empty"); 88 + } 89 + key_content 90 + }; 91 + 92 + // Secure access token handling: prefer environment variable, then secure prompt 93 + let access_token = if let Some(env_token) = args.access_token { 94 + SecretString::new(env_token.into()) 95 + } else { 96 + // Use secure prompt with hidden input 97 + print!("Enter access token: "); 98 + io::stdout().flush()?; 99 + let token = read_password()?; 100 + if token.is_empty() { 101 + anyhow::bail!("Access token cannot be empty"); 102 + } 103 + SecretString::new(token.into()) 104 + }; 105 + let xrpc_path_with_prefix = &args.xrpc_path; 69 106 70 107 // Parse the xrpc_path prefix (optional, defaults to query:) 71 108 let (is_procedure, xrpc_path) = if let Some(path) = xrpc_path_with_prefix.strip_prefix("query:") ··· 82 119 let mut query_params = HashMap::new(); 83 120 let mut header_params = HashMap::new(); 84 121 let mut json_data: Option<serde_json::Value> = None; 85 - let mut arg_index = 4; 122 + let mut arg_index = 0; 86 123 87 124 // For procedure calls, expect the next argument to be a file path 88 125 if is_procedure { 89 - if arg_index >= args.len() { 126 + if arg_index >= args.additional_args.len() { 90 127 anyhow::bail!("procedure: prefix requires a JSON file path as the next argument"); 91 128 } 92 - let file_path = &args[arg_index]; 129 + let file_path = &args.additional_args[arg_index]; 93 130 let file_content = std::fs::read_to_string(file_path) 94 131 .map_err(|e| anyhow::anyhow!("Failed to read file '{}': {}", file_path, e))?; 95 132 json_data = Some(serde_json::from_str(&file_content).map_err(|e| { ··· 99 136 } 100 137 101 138 // Parse remaining key=value arguments and header=name=value arguments 102 - for arg in &args[arg_index..] { 139 + for arg in &args.additional_args[arg_index..] { 103 140 if let Some((key, value)) = arg.split_once('=') { 104 141 if key == "header" { 105 142 // Parse header=name=value format ··· 193 230 println!("Using PDS endpoint: {}", pds_endpoint); 194 231 195 232 // Parse private keys 196 - let private_dpop_key_data = identify_key(private_dpop_key)?; 233 + let private_dpop_key_data = identify_key(&private_dpop_key)?; 197 234 198 235 // Construct the URL 199 236 let mut url = format!("{}/xrpc/{}", pds_endpoint, xrpc_path); ··· 213 250 // Create DPoP auth 214 251 let dpop_auth = DPoPAuth { 215 252 dpop_private_key_data: private_dpop_key_data, 216 - oauth_access_token: access_token.to_string(), 253 + oauth_access_token: access_token.expose_secret().to_string(), 217 254 }; 218 255 219 256 // Create HeaderMap from header parameters
+1 -1
crates/atproto-client/src/com_atproto_server.rs
··· 208 208 "name": name 209 209 }); 210 210 211 - // Create a new client with the access token in Authorization header 211 + // Create a new client with the access token in Authorization header 212 212 let mut headers = reqwest::header::HeaderMap::new(); 213 213 headers.insert( 214 214 reqwest::header::AUTHORIZATION,
+6
crates/atproto-identity/Cargo.toml
··· 19 19 test = false 20 20 bench = false 21 21 doc = true 22 + required-features = ["clap"] 22 23 23 24 [[bin]] 24 25 name = "atproto-identity-sign" 25 26 test = false 26 27 bench = false 27 28 doc = true 29 + required-features = ["clap"] 28 30 29 31 [[bin]] 30 32 name = "atproto-identity-validate" 31 33 test = false 32 34 bench = false 33 35 doc = true 36 + required-features = ["clap"] 34 37 35 38 [[bin]] 36 39 name = "atproto-identity-key" 37 40 test = false 38 41 bench = false 39 42 doc = true 43 + required-features = ["clap"] 40 44 41 45 [dependencies] 42 46 anyhow.workspace = true ··· 56 60 rand.workspace = true 57 61 async-trait = "0.1.88" 58 62 lru = { workspace = true, optional = true } 63 + clap = { workspace = true, optional = true } 59 64 60 65 axum = { version = "0.8", optional = true, features = ["macros"] } 61 66 http = { version = "1.0.0", optional = true } ··· 64 69 default = ["lru", "axum"] 65 70 lru = ["dep:lru"] 66 71 axum = ["dep:axum", "dep:http"] 72 + clap = ["dep:clap"] 67 73 68 74 [lints] 69 75 workspace = true
+69 -131
crates/atproto-identity/src/bin/atproto-identity-key.rs
··· 1 1 //! CLI tool for AT Protocol cryptographic key operations. 2 2 3 - use anyhow::{Result, anyhow}; 3 + use anyhow::Result; 4 4 use atproto_identity::key::{KeyType, generate_key, to_public}; 5 - use std::env; 5 + use clap::{Parser, Subcommand}; 6 6 7 7 /// AT Protocol Key Management Tool 8 - /// 9 - /// This command-line tool provides cryptographic key management capabilities for AT Protocol. 10 - /// It supports key generation and inspection operations for both P-256 and K-256 elliptic curves. 11 - /// 12 - /// ## Overview 13 - /// 14 - /// The tool provides two main subcommands: 15 - /// - **generate**: Creates new cryptographic private keys 16 - /// - **inspect**: Analyzes existing keys (placeholder functionality) 17 - /// 18 - /// ## Key Types 19 - /// 20 - /// - **P-256** (secp256r1/ES256): NIST standard curve, commonly used in web standards 21 - /// - **K-256** (secp256k1/ES256K): Bitcoin curve, widely used in blockchain applications 22 - /// 23 - /// ## Subcommands 24 - /// 25 - /// ### Generate 26 - /// Creates a new private key of the specified type and outputs "OK" on success. 27 - /// 28 - /// ### Inspect 29 - /// Placeholder command that outputs "hello world" (not yet implemented). 30 - /// 31 - /// ## Examples 32 - /// 33 - /// ### Generate P-256 Private Key 34 - /// ```bash 35 - /// atproto-identity-key generate p256 36 - /// ``` 37 - /// 38 - /// ### Generate K-256 Private Key 39 - /// ```bash 40 - /// atproto-identity-key generate k256 41 - /// ``` 42 - /// 43 - /// ### Inspect Key (Placeholder) 44 - /// ```bash 45 - /// atproto-identity-key inspect 46 - /// ``` 47 - /// 48 - /// ## Security Considerations 49 - /// 50 - /// - Generated keys use cryptographically secure random number generation 51 - /// - Private keys are generated using industry-standard elliptic curve implementations 52 - /// - Key material is handled securely in memory 53 - #[tokio::main] 54 - async fn main() -> Result<()> { 55 - let args: Vec<String> = env::args().skip(1).collect(); 8 + #[derive(Parser)] 9 + #[command( 10 + name = "atproto-identity-key", 11 + version, 12 + about = "AT Protocol cryptographic key management", 13 + long_about = " 14 + This command-line tool provides cryptographic key management capabilities for AT Protocol. 15 + It supports key generation and inspection operations for both P-256 and K-256 elliptic curves. 56 16 57 - // Check for help flags or no arguments 58 - if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") { 59 - print_help(); 60 - return Ok(()); 61 - } 62 - 63 - match args.first().map(|s| s.as_str()) { 64 - Some("generate") => handle_generate(&args[1..]).await, 65 - Some("inspect") => handle_inspect(&args[1..]).await, 66 - Some(cmd) => Err(anyhow!( 67 - "Unknown subcommand: {}. Use --help for usage information.", 68 - cmd 69 - )), 70 - None => { 71 - print_help(); 72 - Ok(()) 73 - } 74 - } 75 - } 17 + KEY TYPES: 18 + p256 P-256 (secp256r1/ES256): NIST standard curve, commonly used in web standards 19 + k256 K-256 (secp256k1/ES256K): Bitcoin curve, widely used in blockchain applications 76 20 77 - /// Handles the generate subcommand 78 - async fn handle_generate(args: &[String]) -> Result<()> { 79 - if args.is_empty() { 80 - return Err(anyhow!( 81 - "Generate subcommand requires a key type (p256 or k256)" 82 - )); 83 - } 21 + SECURITY CONSIDERATIONS: 22 + - Generated keys use cryptographically secure random number generation 23 + - Private keys are generated using industry-standard elliptic curve implementations 24 + - Key material is handled securely in memory 84 25 85 - let key_type_str = &args[0]; 86 - let key_type = match key_type_str.as_str() { 87 - "p256" => KeyType::P256Private, 88 - "k256" => KeyType::K256Private, 89 - _ => { 90 - return Err(anyhow!( 91 - "Invalid key type: {}. Supported types: p256, k256", 92 - key_type_str 93 - )); 94 - } 95 - }; 26 + EXAMPLES: 27 + # Generate P-256 key pair: 28 + atproto-identity-key generate p256 96 29 97 - let private_key = generate_key(key_type)?; 98 - let public_key = to_public(&private_key)?; 30 + # Generate K-256 key pair: 31 + atproto-identity-key generate k256 99 32 100 - println!("{} private: {}", key_type_str, private_key); 101 - println!("{} public: {}", key_type_str, public_key); 33 + # Inspect key (placeholder): 34 + atproto-identity-key inspect 35 + " 36 + )] 37 + struct Args { 38 + #[command(subcommand)] 39 + command: Commands, 40 + } 102 41 103 - Ok(()) 42 + #[derive(Subcommand)] 43 + enum Commands { 44 + /// Generate a new key pair and display both private and public keys 45 + Generate { 46 + /// Key type to generate (p256 or k256) 47 + #[arg(value_enum)] 48 + key_type: KeyTypeArg, 49 + }, 50 + /// Inspect key information (placeholder) 51 + Inspect, 104 52 } 105 53 106 - /// Handles the inspect subcommand (placeholder) 107 - async fn handle_inspect(_args: &[String]) -> Result<()> { 108 - println!("hello world"); 109 - Ok(()) 54 + #[derive(clap::ValueEnum, Clone)] 55 + enum KeyTypeArg { 56 + /// P-256 (secp256r1/ES256) key pair 57 + P256, 58 + /// K-256 (secp256k1/ES256K) key pair 59 + K256, 110 60 } 61 + #[tokio::main] 62 + async fn main() -> Result<()> { 63 + let args = Args::parse(); 111 64 112 - /// Prints help information 113 - fn print_help() { 114 - println!("AT Protocol Key Management Tool"); 115 - println!(); 116 - println!("USAGE:"); 117 - println!(" atproto-identity-key <SUBCOMMAND> [OPTIONS]"); 118 - println!(); 119 - println!("SUBCOMMANDS:"); 120 - println!( 121 - " generate <KEY_TYPE> Generate a new key pair and display both private and public keys" 122 - ); 123 - println!(" inspect Inspect key information (placeholder)"); 124 - println!(); 125 - println!("KEY TYPES:"); 126 - println!(" p256 Generate P-256 (secp256r1/ES256) key pair"); 127 - println!(" k256 Generate K-256 (secp256k1/ES256K) key pair"); 128 - println!(); 129 - println!("EXAMPLES:"); 130 - println!(" # Generate P-256 key pair:"); 131 - println!(" atproto-identity-key generate p256"); 132 - println!(" # Output:"); 133 - println!(" # p256 private: did:key:z42..."); 134 - println!(" # p256 public: did:key:z4oJ..."); 135 - println!(); 136 - println!(" # Generate K-256 key pair:"); 137 - println!(" atproto-identity-key generate k256"); 138 - println!(" # Output:"); 139 - println!(" # k256 private: did:key:z3vL..."); 140 - println!(" # k256 public: did:key:zQ3s..."); 141 - println!(); 142 - println!(" # Inspect key (placeholder):"); 143 - println!(" atproto-identity-key inspect"); 144 - println!(); 145 - println!("OPTIONS:"); 146 - println!(" -h, --help Print this help information"); 65 + match args.command { 66 + Commands::Generate { key_type } => { 67 + let (key_type_actual, key_type_str) = match key_type { 68 + KeyTypeArg::P256 => (KeyType::P256Private, "p256"), 69 + KeyTypeArg::K256 => (KeyType::K256Private, "k256"), 70 + }; 71 + 72 + let private_key = generate_key(key_type_actual)?; 73 + let public_key = to_public(&private_key)?; 74 + 75 + println!("{} private: {}", key_type_str, private_key); 76 + println!("{} public: {}", key_type_str, public_key); 77 + 78 + Ok(()) 79 + } 80 + Commands::Inspect => { 81 + println!("hello world"); 82 + Ok(()) 83 + } 84 + } 147 85 }
+40 -71
crates/atproto-identity/src/bin/atproto-identity-resolve.rs
··· 2 2 //! 3 3 //! This tool can resolve both handles and DIDs to their identity documents. 4 4 5 - use std::env; 6 - 7 5 use anyhow::Result; 8 6 use atproto_identity::{ 9 7 config::{CertificateBundles, DnsNameservers, default_env, optional_env, version}, ··· 11 9 resolve::{InputType, create_resolver, parse_input, resolve_subject}, 12 10 web::query as web_query, 13 11 }; 12 + use clap::Parser; 14 13 15 14 /// AT Protocol Identity Resolution CLI 16 - /// 17 - /// A command-line tool for resolving AT Protocol handles and DIDs to their canonical 18 - /// DID identifiers and optionally retrieving their full DID documents. Supports 19 - /// both did:plc and did:web methods with configurable DNS and HTTP settings. 20 - /// 21 - /// ## Usage 22 - /// 23 - /// Resolve one or more handles or DIDs: 24 - /// ```bash 25 - /// cargo run --bin atproto-identity-resolve alice.bsky.social 26 - /// cargo run --bin atproto-identity-resolve did:plc:ewvi7nxzyoun6zhxrhs64oiz 27 - /// cargo run --bin atproto-identity-resolve alice.bsky.social bob.example.com 28 - /// ``` 29 - /// 30 - /// Resolve and fetch DID documents: 31 - /// ```bash 32 - /// cargo run --bin atproto-identity-resolve --did-document alice.bsky.social 33 - /// cargo run --bin atproto-identity-resolve --did-document did:web:example.com 34 - /// ``` 35 - /// 36 - /// ## Arguments 37 - /// 38 - /// - `[SUBJECTS]...` - One or more AT Protocol handles or DIDs to resolve 39 - /// - `--did-document` - Additionally fetch and display the full DID document 40 - /// 41 - /// ## Environment Variables 42 - /// 43 - /// - `PLC_HOSTNAME` - PLC directory hostname (default: "plc.directory") 44 - /// - `USER_AGENT` - HTTP user agent string (default: auto-generated) 45 - /// - `CERTIFICATE_BUNDLES` - Colon-separated paths to additional CA certificates 46 - /// - `DNS_NAMESERVERS` - Comma-separated DNS nameserver addresses 47 - /// 48 - /// ## Examples 49 - /// 50 - /// Basic handle resolution: 51 - /// ```bash 52 - /// $ cargo run --bin atproto-identity-resolve alice.bsky.social 53 - /// did:plc:ewvi7nxzyoun6zhxrhs64oiz 54 - /// ``` 55 - /// 56 - /// Resolve multiple subjects: 57 - /// ```bash 58 - /// $ cargo run --bin atproto-identity-resolve alice.bsky.social bob.example.com 59 - /// did:plc:ewvi7nxzyoun6zhxrhs64oiz 60 - /// did:web:bob.example.com 61 - /// ``` 62 - /// 63 - /// Get DID document: 64 - /// ```bash 65 - /// $ cargo run --bin atproto-identity-resolve --did-document alice.bsky.social 66 - /// did:plc:ewvi7nxzyoun6zhxrhs64oiz 67 - /// PlcDocument { did: "did:plc:ewvi7nxzyoun6zhxrhs64oiz", ... } 68 - /// ``` 15 + #[derive(Parser)] 16 + #[command( 17 + name = "atproto-identity-resolve", 18 + version, 19 + about = "Resolve AT Protocol handles and DIDs to their canonical DID identifiers", 20 + long_about = " 21 + A command-line tool for resolving AT Protocol handles and DIDs to their canonical 22 + DID identifiers and optionally retrieving their full DID documents. Supports 23 + both did:plc and did:web methods with configurable DNS and HTTP settings. 24 + 25 + ENVIRONMENT VARIABLES: 26 + PLC_HOSTNAME PLC directory hostname (default: \"plc.directory\") 27 + USER_AGENT HTTP user agent string (default: auto-generated) 28 + CERTIFICATE_BUNDLES Colon-separated paths to additional CA certificates 29 + DNS_NAMESERVERS Comma-separated DNS nameserver addresses 30 + 31 + EXAMPLES: 32 + # Basic handle resolution: 33 + atproto-identity-resolve alice.bsky.social 34 + 35 + # Resolve multiple subjects: 36 + atproto-identity-resolve alice.bsky.social bob.example.com 37 + 38 + # Get DID document: 39 + atproto-identity-resolve --did-document alice.bsky.social 40 + " 41 + )] 42 + struct Args { 43 + /// One or more AT Protocol handles or DIDs to resolve 44 + subjects: Vec<String>, 45 + 46 + /// Additionally fetch and display the full DID document 47 + #[arg(long)] 48 + did_document: bool, 49 + } 69 50 #[tokio::main] 70 51 async fn main() -> Result<()> { 52 + let args = Args::parse(); 53 + 71 54 let plc_hostname = default_env("PLC_HOSTNAME", "plc.directory"); 72 55 let certificate_bundles: CertificateBundles = optional_env("CERTIFICATE_BUNDLES").try_into()?; 73 56 let default_user_agent = format!( ··· 89 72 90 73 let dns_resolver = create_resolver(dns_nameservers.as_ref()); 91 74 92 - let mut subjects = vec![]; 93 - let mut get_did_document = false; 94 - 95 - for subject in env::args().skip(1) { 96 - match subject.as_str() { 97 - "--did-document" => { 98 - get_did_document = true; 99 - } 100 - _ => { 101 - subjects.push(subject); 102 - } 103 - } 104 - } 105 - 106 - for subject in subjects { 75 + for subject in args.subjects { 107 76 let resolved_did = resolve_subject(&http_client, &dns_resolver, &subject).await; 108 77 let resolved_did = match resolved_did { 109 78 Ok(value) => { ··· 116 85 } 117 86 }; 118 87 119 - if !get_did_document { 88 + if !args.did_document { 120 89 continue; 121 90 } 122 91
+48 -73
crates/atproto-identity/src/bin/atproto-identity-sign.rs
··· 1 1 //! CLI tool for signing data with cryptographic keys. 2 2 3 - use anyhow::{Context, Result, anyhow}; 3 + use anyhow::{Context, Result}; 4 4 use atproto_identity::key::{identify_key, sign}; 5 - use std::{env, fs::File, io::BufReader}; 5 + use clap::Parser; 6 + use std::process::ExitCode; 7 + use std::{fs::File, io::BufReader}; 8 + 9 + /// AT Protocol Digital Signature CLI 10 + #[derive(Parser)] 11 + #[command( 12 + name = "atproto-identity-sign", 13 + version, 14 + about = "Create cryptographic signatures of JSON data using AT Protocol DID keys", 15 + long_about = " 16 + A command-line tool for creating cryptographic signatures of JSON data using 17 + AT Protocol DID keys. Takes a JSON file, serializes it using IPLD DAG-CBOR 18 + format, and produces a multibase-encoded signature using the specified key. 19 + Supports both P-256 and K-256 elliptic curve keys via did:key method. 20 + 21 + PROCESS: 22 + 1. Parses and validates the provided DID key 23 + 2. Reads and parses the JSON file 24 + 3. Serializes the JSON data using IPLD DAG-CBOR encoding 25 + 4. Creates a cryptographic signature using the specified key 26 + 5. Encodes the signature using multibase Base64URL format 27 + 6. Outputs the encoded signature to stdout 28 + 29 + SUPPORTED KEY TYPES: 30 + - P-256 (secp256r1) - NIST standard elliptic curve 31 + - K-256 (secp256k1) - Bitcoin/Ethereum compatible curve 32 + 33 + EXAMPLES: 34 + # Create test data and sign it: 35 + echo '{\"message\": \"hello world\"}' > data.json 36 + atproto-identity-sign did:key:z42tv1pb3... data.json 6 37 7 - use std::process::ExitCode; 38 + # Using jo to create JSON: 39 + jo message=\"hello world\" when=\"$(date)\" > message.json 40 + atproto-identity-sign did:key:z42tv1pb3... message.json 41 + " 42 + )] 43 + struct Args { 44 + /// A did:key identifier containing the signing key 45 + did_key: String, 8 46 9 - async fn real_main() -> Result<()> { 10 - let mut arguments = env::args().skip(1); //. .collect::<Vec<String>>(); 47 + /// Path to a JSON file containing the data to sign 48 + json_file: String, 49 + } 11 50 12 - if arguments.len() != 2 { 13 - return Err(anyhow!("Usage: atproto-identity-sign [did-key] [file]")); 14 - } 51 + async fn real_main() -> Result<()> { 52 + let args = Args::parse(); 15 53 16 - let key_data = arguments 17 - .next() 18 - .ok_or(anyhow!("missing did-method-key value"))?; 19 - let identified_key = identify_key(&key_data)?; 54 + let identified_key = identify_key(&args.did_key)?; 20 55 21 - let json_file_path = arguments.next().ok_or(anyhow!("missing json-file value"))?; 22 - let record: serde_json::Value = File::open(json_file_path) 56 + let record: serde_json::Value = File::open(&args.json_file) 23 57 .context("failed to open json file") 24 58 .map(BufReader::new) 25 59 .and_then(|value| serde_json::from_reader(value).context("failed to parse json file"))?; ··· 34 68 Ok(()) 35 69 } 36 70 37 - /// AT Protocol Digital Signature CLI 38 - /// 39 - /// A command-line tool for creating cryptographic signatures of JSON data using 40 - /// AT Protocol DID keys. Takes a JSON file, serializes it using IPLD DAG-CBOR 41 - /// format, and produces a multibase-encoded signature using the specified key. 42 - /// Supports both P-256 and K-256 elliptic curve keys via did:key method. 43 - /// 44 - /// ## Usage 45 - /// 46 - /// ```bash 47 - /// cargo run --bin atproto-identity-sign [DID_KEY] [JSON_FILE] 48 - /// ``` 49 - /// 50 - /// ## Arguments 51 - /// 52 - /// - `DID_KEY` - A did:key identifier containing the signing key (e.g., did:key:z42tv1pb3...) 53 - /// - `JSON_FILE` - Path to a JSON file containing the data to sign 54 - /// 55 - /// ## Process 56 - /// 57 - /// 1. Parses and validates the provided DID key 58 - /// 2. Reads and parses the JSON file 59 - /// 3. Serializes the JSON data using IPLD DAG-CBOR encoding 60 - /// 4. Creates a cryptographic signature using the specified key 61 - /// 5. Encodes the signature using multibase Base64URL format 62 - /// 6. Outputs the encoded signature to stdout 63 - /// 64 - /// ## Examples 65 - /// 66 - /// Create a sample JSON file and sign it: 67 - /// ```bash 68 - /// # Create test data 69 - /// $ echo '{"message": "hello world", "timestamp": "2024-01-01T00:00:00Z"}' > data.json 70 - /// 71 - /// # Sign the data 72 - /// $ cargo run --bin atproto-identity-sign did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW data.json 73 - /// uEiABC123...XYZ789 74 - /// ``` 75 - /// 76 - /// Using jo to create JSON and sign in one command: 77 - /// ```bash 78 - /// $ jo message="hello world" when="$(date)" > message.json 79 - /// $ cargo run --bin atproto-identity-sign did:key:z42tv1pb3... message.json 80 - /// uEiBDE456...ABC123 81 - /// ``` 82 - /// 83 - /// ## Supported Key Types 84 - /// 85 - /// - **P-256 (secp256r1)** - NIST standard elliptic curve 86 - /// - **K-256 (secp256k1)** - Bitcoin/Ethereum compatible curve 87 - /// 88 - /// ## Error Handling 89 - /// 90 - /// - Invalid DID key format or unsupported key type 91 - /// - Missing or unreadable JSON file 92 - /// - Malformed JSON content 93 - /// - Cryptographic signing failures 94 - /// 95 - /// All errors are reported to stderr with descriptive messages. 96 71 #[tokio::main] 97 72 async fn main() -> ExitCode { 98 73 if let Err(err) = real_main().await {
+55 -92
crates/atproto-identity/src/bin/atproto-identity-validate.rs
··· 1 1 //! CLI tool for validating cryptographic signatures. 2 2 3 - use anyhow::{Context, Result, anyhow}; 3 + use anyhow::{Context, Result}; 4 4 use atproto_identity::key::{identify_key, validate}; 5 - use std::{env, fs::File, io::BufReader}; 5 + use clap::Parser; 6 + use std::{fs::File, io::BufReader, process::ExitCode}; 6 7 7 - use std::process::ExitCode; 8 + /// AT Protocol Signature Validation CLI 9 + #[derive(Parser)] 10 + #[command( 11 + name = "atproto-identity-validate", 12 + version, 13 + about = "Validate cryptographic signatures of JSON data using AT Protocol DID keys", 14 + long_about = " 15 + A command-line tool for verifying cryptographic signatures of JSON data using 16 + AT Protocol DID keys. Takes a JSON file, a multibase-encoded signature, and 17 + a DID key, then validates that the signature was created by the specified key 18 + for the given data. Uses IPLD DAG-CBOR serialization format for verification. 19 + Supports both P-256 and K-256 elliptic curve keys via did:key method. 8 20 9 - async fn real_main() -> Result<()> { 10 - let mut arguments = env::args().skip(1); //. .collect::<Vec<String>>(); 21 + EXAMPLES: 22 + # Basic signature validation: 23 + atproto-identity-validate did:key:z42tv1pb3... data.json uEiABC123... 11 24 12 - if arguments.len() != 3 { 13 - return Err(anyhow!( 14 - "Usage: atproto-identity-validate [did-key] [file] [signature]" 15 - )); 16 - } 25 + # Validate signature from external source: 26 + atproto-identity-validate \\ 27 + did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\ 28 + message.json \\ 29 + uEiABC123defGHI456jklMNO789pqrSTU012vwxYZ345abc 17 30 18 - let key_data = arguments 19 - .next() 20 - .ok_or(anyhow!("missing did-method-key value"))?; 21 - let identified_key = identify_key(&key_data)?; 31 + SUPPORTED KEY TYPES: 32 + - P-256 (secp256r1) - NIST standard elliptic curve 33 + - K-256 (secp256k1) - Bitcoin/Ethereum compatible curve 22 34 23 - let json_file_path = arguments.next().ok_or(anyhow!("missing json-file value"))?; 24 - let record: serde_json::Value = File::open(json_file_path) 35 + ERROR HANDLING: 36 + - Invalid DID key format or unsupported key type 37 + - Missing or unreadable JSON file 38 + - Malformed JSON content 39 + - Invalid multibase-encoded signature 40 + - Signature validation failure 41 + 42 + All errors are reported to stderr with descriptive messages and return exit code 1. 43 + Successful validation prints \"OK\" to stdout and returns exit code 0. 44 + " 45 + )] 46 + struct Args { 47 + /// DID key identifier containing the verification key (e.g., did:key:z42tv1pb3...) 48 + did_key: String, 49 + 50 + /// Path to a JSON file containing the original signed data 51 + json_file: String, 52 + 53 + /// Multibase-encoded signature to validate (typically Base64URL) 54 + signature: String, 55 + } 56 + 57 + async fn real_main() -> Result<()> { 58 + let args = Args::parse(); 59 + 60 + let identified_key = identify_key(&args.did_key)?; 61 + 62 + let record: serde_json::Value = File::open(&args.json_file) 25 63 .context("failed to open json file") 26 64 .map(BufReader::new) 27 65 .and_then(|value| serde_json::from_reader(value).context("failed to parse json file"))?; 28 66 29 67 let serialized_record = serde_ipld_dagcbor::to_vec(&record)?; 30 68 31 - let encoded_signature = arguments.next().ok_or(anyhow!("missing signature value"))?; 32 - 33 - let (_, signature_data) = multibase::decode(encoded_signature)?; 69 + let (_, signature_data) = multibase::decode(&args.signature)?; 34 70 35 71 validate(&identified_key, &signature_data, &serialized_record)?; 36 72 37 73 Ok(()) 38 74 } 39 75 40 - /// AT Protocol Signature Validation CLI 41 - /// 42 - /// A command-line tool for verifying cryptographic signatures of JSON data using 43 - /// AT Protocol DID keys. Takes a JSON file, a multibase-encoded signature, and 44 - /// a DID key, then validates that the signature was created by the specified key 45 - /// for the given data. Uses IPLD DAG-CBOR serialization format for verification. 46 - /// Supports both P-256 and K-256 elliptic curve keys via did:key method. 47 - /// 48 - /// ## Usage 49 - /// 50 - /// ```bash 51 - /// cargo run --bin atproto-identity-validate [DID_KEY] [JSON_FILE] [SIGNATURE] 52 - /// ``` 53 - /// 54 - /// ## Arguments 55 - /// 56 - /// - `DID_KEY` - A did:key identifier containing the verification key (e.g., did:key:z42tv1pb3...) 57 - /// - `JSON_FILE` - Path to a JSON file containing the original signed data 58 - /// - `SIGNATURE` - Multibase-encoded signature to validate (typically Base64URL) 59 - /// 60 - /// ## Process 61 - /// 62 - /// 1. Parses and validates the provided DID key 63 - /// 2. Reads and parses the JSON file 64 - /// 3. Serializes the JSON data using IPLD DAG-CBOR encoding (same as signing) 65 - /// 4. Decodes the multibase-encoded signature 66 - /// 5. Verifies the signature against the data using the specified key 67 - /// 6. Outputs "OK" on successful validation, error message on failure 68 - /// 7. Returns exit code 0 for success, 1 for failure 69 - /// 70 - /// ## Examples 71 - /// 72 - /// Basic signature validation workflow: 73 - /// ```bash 74 - /// # Create and sign data 75 - /// $ echo '{"message": "hello world", "timestamp": "2024-01-01T00:00:00Z"}' > data.json 76 - /// $ SIGNATURE=$(cargo run --bin atproto-identity-sign did:key:z42tv1pb3... data.json) 77 - /// 78 - /// # Validate the signature 79 - /// $ cargo run --bin atproto-identity-validate did:key:z42tv1pb3... data.json $SIGNATURE 80 - /// OK 81 - /// ``` 82 - /// 83 - /// Validating a signature from external source: 84 - /// ```bash 85 - /// $ cargo run --bin atproto-identity-validate \ 86 - /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 87 - /// message.json \ 88 - /// uEiABC123defGHI456jklMNO789pqrSTU012vwxYZ345abc 89 - /// OK 90 - /// ``` 91 - /// 92 - /// Invalid signature example: 93 - /// ```bash 94 - /// $ cargo run --bin atproto-identity-validate did:key:z42tv1pb3... data.json invalid_sig 95 - /// error-atproto-identity-key-1 Signature validation failed 96 - /// ``` 97 - /// 98 - /// ## Supported Key Types 99 - /// 100 - /// - **P-256 (secp256r1)** - NIST standard elliptic curve 101 - /// - **K-256 (secp256k1)** - Bitcoin/Ethereum compatible curve 102 - /// 103 - /// ## Error Handling 104 - /// 105 - /// - Invalid DID key format or unsupported key type 106 - /// - Missing or unreadable JSON file 107 - /// - Malformed JSON content 108 - /// - Invalid multibase-encoded signature 109 - /// - Signature validation failure (wrong key, tampered data, etc.) 110 - /// 111 - /// All errors are reported to stderr with descriptive messages and return exit code 1. 112 - /// Successful validation prints "OK" to stdout and returns exit code 0. 113 76 #[tokio::main] 114 77 async fn main() -> ExitCode { 115 78 if let Err(err) = real_main().await {
+11
crates/atproto-jetstream/Cargo.toml
··· 14 14 keywords.workspace = true 15 15 categories.workspace = true 16 16 17 + [[bin]] 18 + name = "atproto-jetstream-consumer" 19 + test = false 20 + bench = false 21 + doc = true 22 + required-features = ["clap"] 23 + 17 24 [dependencies] 18 25 tokio = { workspace = true, features = ["full"] } 19 26 tokio-util.workspace = true ··· 30 37 urlencoding.workspace = true 31 38 tokio-websockets.workspace = true 32 39 http.workspace = true 40 + clap = { workspace = true, optional = true } 41 + 42 + [features] 43 + clap = ["dep:clap"] 33 44 34 45 [lints] 35 46 workspace = true
+51 -40
crates/atproto-jetstream/src/bin/atproto-jetstream-consumer.rs
··· 6 6 use anyhow::Result; 7 7 use atproto_identity::config::{CertificateBundles, default_env, optional_env, version}; 8 8 use atproto_jetstream::{CancellationToken, Consumer, ConsumerTaskConfig, LoggingHandler}; 9 - use std::{env, sync::Arc}; 9 + use clap::Parser; 10 + use std::sync::Arc; 10 11 use tokio::signal; 11 12 12 - fn print_usage() { 13 - println!("AT Protocol Jetstream Consumer Tool"); 14 - println!(); 15 - println!("Usage:"); 16 - println!(" atproto-jetstream-consumer <jetstream_hostname> <zstd_dictionary> [collection...]"); 17 - println!(); 18 - println!("Arguments:"); 19 - println!(" jetstream_hostname Hostname of the Jetstream instance to connect to"); 20 - println!( 21 - " zstd_dictionary Path to zstd dictionary file (use 'none' to disable compression)" 22 - ); 23 - println!(" collection Zero or more AT Protocol collections to subscribe to"); 24 - println!(); 25 - println!("Environment Variables:"); 26 - println!(" CERTIFICATE_BUNDLES Optional path to additional CA certificates"); 27 - println!(" USER_AGENT Custom user agent string"); 28 - println!(); 29 - println!("Examples:"); 30 - println!(" # Subscribe to feed posts with compression"); 31 - println!( 32 - " atproto-jetstream-consumer jetstream1.us-east.bsky.network /path/to/dict.zstd app.bsky.feed.post" 33 - ); 34 - println!(); 35 - println!(" # Subscribe to multiple collections without compression"); 36 - println!( 37 - " atproto-jetstream-consumer jetstream1.us-east.bsky.network none app.bsky.feed.post app.bsky.feed.repost" 38 - ); 39 - println!(); 40 - println!(" # Subscribe to all collections"); 41 - println!(" atproto-jetstream-consumer jetstream1.us-east.bsky.network none"); 13 + /// AT Protocol Jetstream Consumer Tool 14 + #[derive(Parser)] 15 + #[command( 16 + name = "atproto-jetstream-consumer", 17 + version, 18 + about = "Stream AT Protocol events from Jetstream instances", 19 + long_about = " 20 + A command-line tool for connecting to AT Protocol Jetstream instances and 21 + streaming events from specified collections. Supports optional zstd compression 22 + for efficient data transfer. 23 + 24 + COMPRESSION: 25 + Use 'none' as the zstd_dictionary to disable compression, or provide 26 + a path to a zstd dictionary file to enable compression. 27 + 28 + COLLECTIONS: 29 + Specify zero or more AT Protocol collections to subscribe to. If no 30 + collections are specified, subscribes to all collections. 31 + 32 + EXAMPLES: 33 + # Subscribe to feed posts with compression: 34 + atproto-jetstream-consumer jetstream1.us-east.bsky.network \\ 35 + /path/to/dict.zstd app.bsky.feed.post 36 + 37 + # Subscribe to multiple collections without compression: 38 + atproto-jetstream-consumer jetstream1.us-east.bsky.network none \\ 39 + app.bsky.feed.post app.bsky.feed.repost 40 + 41 + # Subscribe to all collections: 42 + atproto-jetstream-consumer jetstream1.us-east.bsky.network none 43 + 44 + ENVIRONMENT VARIABLES: 45 + CERTIFICATE_BUNDLES Additional CA certificate bundles 46 + USER_AGENT Custom user agent string 47 + " 48 + )] 49 + struct Args { 50 + /// Hostname of the Jetstream instance to connect to 51 + jetstream_hostname: String, 52 + 53 + /// Path to zstd dictionary file (use 'none' to disable compression) 54 + zstd_dictionary: String, 55 + 56 + /// Zero or more AT Protocol collections to subscribe to 57 + collections: Vec<String>, 42 58 } 43 59 44 60 #[tokio::main] ··· 49 65 .init(); 50 66 51 67 // Parse command line arguments 52 - let args: Vec<String> = env::args().skip(1).collect(); 53 - 54 - if args.len() < 2 || args.iter().any(|arg| arg == "--help" || arg == "-h") { 55 - print_usage(); 56 - return Ok(()); 57 - } 68 + let args = Args::parse(); 58 69 59 - let jetstream_hostname = &args[0]; 60 - let zstd_dictionary_path = &args[1]; 61 - let collections: Vec<String> = args[2..].iter().map(|s| s.to_string()).collect(); 70 + let jetstream_hostname = &args.jetstream_hostname; 71 + let zstd_dictionary_path = &args.zstd_dictionary; 72 + let collections = args.collections; 62 73 63 74 tracing::info!( 64 75 hostname = %jetstream_hostname,
+13
crates/atproto-oauth-axum/Cargo.toml
··· 14 14 keywords.workspace = true 15 15 categories.workspace = true 16 16 17 + [[bin]] 18 + name = "atproto-oauth-tool" 19 + test = false 20 + bench = false 21 + doc = true 22 + required-features = ["clap"] 23 + 17 24 [dependencies] 18 25 atproto-identity.workspace = true 19 26 atproto-record.workspace = true ··· 36 43 urlencoding = "2.1.3" 37 44 axum = { version = "0.8", features = ["macros"] } 38 45 http = "1.0.0" 46 + clap = { workspace = true, optional = true } 47 + rpassword = { workspace = true, optional = true } 48 + secrecy = { workspace = true, optional = true } 49 + 50 + [features] 51 + clap = ["dep:clap", "dep:rpassword", "dep:secrecy"] 39 52 40 53 [lints] 41 54 workspace = true
+110 -50
crates/atproto-oauth-axum/src/bin/atproto-oauth-tool.rs
··· 49 49 use atproto_oauth_axum::{handler_metadata::handle_oauth_metadata, state::OAuthClientConfig}; 50 50 use axum::{Router, extract::FromRef, routing::get}; 51 51 use chrono::{Duration, Utc}; 52 + use clap::{Parser, Subcommand}; 52 53 use hickory_resolver::TokioResolver; 53 54 use rand::distributions::{Alphanumeric, DistString}; 54 - use std::{collections::HashMap, env, num::NonZeroUsize, ops::Deref, sync::Arc}; 55 + use rpassword::read_password; 56 + use secrecy::{ExposeSecret, SecretString}; 57 + use std::{collections::HashMap, env, io::{self, Write}, num::NonZeroUsize, ops::Deref, sync::Arc}; 55 58 56 59 #[derive(Clone)] 57 60 pub struct SimpleKeyProvider { ··· 129 132 } 130 133 } 131 134 132 - fn print_usage() { 133 - println!("AT Protocol OAuth Tool"); 134 - println!(); 135 - println!("Usage:"); 136 - println!(" atproto-oauth-tool login <private_signing_key> <subject>"); 137 - println!( 138 - " atproto-oauth-tool refresh <private_signing_key> <subject> <private_dpop_key> <refresh_token>" 139 - ); 140 - println!(); 141 - println!("Commands:"); 142 - println!(" login Start OAuth login flow"); 143 - println!(" refresh Refresh OAuth tokens"); 144 - println!(); 145 - println!("Arguments:"); 146 - println!(" private_signing_key Private key for signing"); 147 - println!(" subject OAuth subject identifier"); 148 - println!(" private_dpop_key Private DPoP key (refresh only)"); 149 - println!(" refresh_token Refresh token (refresh only)"); 135 + /// AT Protocol OAuth Tool 136 + #[derive(Parser)] 137 + #[command( 138 + name = "atproto-oauth-tool", 139 + version, 140 + about = "Manage AT Protocol OAuth authentication flows", 141 + long_about = " 142 + A command-line tool for managing AT Protocol OAuth authentication flows. 143 + Provides functionality to initiate OAuth login flows and refresh access tokens 144 + for AT Protocol services. 145 + 146 + FEATURES: 147 + - Complete OAuth 2.0 authorization code flow with PKCE 148 + - DPoP (Demonstration of Proof-of-Possession) token support 149 + - AT Protocol identity resolution and DID document management 150 + - OAuth server endpoints for client metadata and callback handling 151 + - Support for both did:plc and did:web identity methods 152 + 153 + ENVIRONMENT VARIABLES: 154 + EXTERNAL_BASE External hostname for OAuth endpoints (required) 155 + PORT HTTP server port (default: 8080) 156 + PLC_HOSTNAME PLC directory hostname (default: plc.directory) 157 + USER_AGENT HTTP User-Agent header (auto-generated) 158 + DNS_NAMESERVERS Custom DNS nameservers (optional) 159 + CERTIFICATE_BUNDLES Additional CA certificates (optional) 160 + " 161 + )] 162 + struct Args { 163 + #[command(subcommand)] 164 + command: Commands, 165 + } 166 + 167 + #[derive(Subcommand)] 168 + enum Commands { 169 + /// Start OAuth login flow 170 + Login { 171 + /// OAuth subject identifier 172 + subject: String, 173 + }, 174 + /// Refresh OAuth tokens 175 + Refresh { 176 + /// OAuth subject identifier 177 + subject: String, 178 + }, 150 179 } 151 180 152 181 #[tokio::main] 153 182 async fn main() -> Result<()> { 154 - // Parse command line arguments 155 - let args: Vec<String> = env::args().skip(1).collect(); 156 - 157 - if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") { 158 - print_usage(); 159 - return Ok(()); 160 - } 183 + let args = Args::parse(); 161 184 185 + // Handle secure credential loading based on command 162 186 let (subcommand, private_signing_key, subject, private_dpop_key, refresh_token) = 163 - match args.len() { 164 - 3 if args[0] == "login" => ("login", args[1].clone(), args[2].clone(), None, None), 165 - 5 if args[0] == "refresh" => ( 166 - "refresh", 167 - args[1].clone(), 168 - args[2].clone(), 169 - Some(args[3].clone()), 170 - Some(args[4].clone()), 171 - ), 172 - _ => { 173 - eprintln!("Error: Invalid arguments"); 174 - print_usage(); 175 - std::process::exit(1); 176 - } 187 + match &args.command { 188 + Commands::Login { subject } => { 189 + let signing_key = load_secret_from_env_or_prompt("ATPROTO_SIGNING_KEY", "signing key")?; 190 + ( 191 + "login", 192 + signing_key, 193 + subject.clone(), 194 + None, 195 + None, 196 + ) 197 + }, 198 + Commands::Refresh { subject } => { 199 + let signing_key = load_secret_from_env_or_prompt("ATPROTO_SIGNING_KEY", "signing key")?; 200 + let dpop_key = load_secret_from_env_or_prompt("ATPROTO_DPOP_KEY", "DPoP key")?; 201 + let token = load_secret_string_from_env_or_prompt("ATPROTO_REFRESH_TOKEN", "refresh token")?; 202 + ( 203 + "refresh", 204 + signing_key, 205 + subject.clone(), 206 + Some(dpop_key), 207 + Some(token), 208 + ) 209 + }, 177 210 }; 178 211 179 - // Log the selected command 212 + // Log the selected command (without sensitive data) 180 213 println!( 181 214 "Starting OAuth {} flow for subject: {}", 182 215 subcommand, subject 183 216 ); 184 - if let Some(ref dpop_key) = private_dpop_key { 185 - println!("Using DPoP key: {}", dpop_key); 217 + if private_dpop_key.is_some() { 218 + println!("DPoP key loaded from secure source"); 186 219 } 187 - if let Some(ref token) = refresh_token { 188 - println!( 189 - "Using refresh token: {}...", 190 - &token[..std::cmp::min(8, token.len())] 191 - ); 220 + if refresh_token.is_some() { 221 + println!("Refresh token loaded from secure source"); 192 222 } 193 223 194 224 let plc_hostname = default_env("PLC_HOSTNAME", "plc.directory"); ··· 465 495 external_base: &str, 466 496 plc_hostname: &str, 467 497 private_dpop_key: &str, 468 - refresh_token: &str, 498 + refresh_token: &SecretString, 469 499 did_document_storage: &Arc<dyn DidDocumentStorage + Send + Sync>, 470 500 ) -> Result<()> { 471 501 println!("Resolving subject: {}", subject); ··· 526 556 http_client, 527 557 &oauth_client, 528 558 &dpop_key, 529 - refresh_token, 559 + refresh_token.expose_secret(), 530 560 &document, 531 561 ) 532 562 .await ··· 544 574 545 575 Ok(()) 546 576 } 577 + 578 + /// Load secret from environment variable or secure prompt 579 + fn load_secret_from_env_or_prompt(env_var: &str, secret_type: &str) -> Result<String> { 580 + if let Ok(secret) = env::var(env_var) { 581 + Ok(secret) 582 + } else { 583 + print!("Enter {} content: ", secret_type); 584 + io::stdout().flush()?; 585 + let secret = read_password()?; 586 + if secret.is_empty() { 587 + anyhow::bail!("{} cannot be empty", secret_type); 588 + } 589 + Ok(secret) 590 + } 591 + } 592 + 593 + /// Load secret string from environment variable or secure prompt 594 + fn load_secret_string_from_env_or_prompt(env_var: &str, secret_type: &str) -> Result<SecretString> { 595 + if let Ok(secret) = env::var(env_var) { 596 + Ok(SecretString::new(secret.into())) 597 + } else { 598 + print!("Enter {} content: ", secret_type); 599 + io::stdout().flush()?; 600 + let secret = read_password()?; 601 + if secret.is_empty() { 602 + anyhow::bail!("{} cannot be empty", secret_type); 603 + } 604 + Ok(SecretString::new(secret.into())) 605 + } 606 + }
+6
crates/atproto-record/Cargo.toml
··· 19 19 test = false 20 20 bench = false 21 21 doc = true 22 + required-features = ["clap"] 22 23 23 24 [[bin]] 24 25 name = "atproto-record-verify" 25 26 test = false 26 27 bench = false 27 28 doc = true 29 + required-features = ["clap"] 28 30 29 31 [dependencies] 30 32 atproto-identity.workspace = true ··· 42 44 43 45 tokio = {workspace = true} 44 46 chrono = {version = "0.4.41", default-features = false, features = ["std", "now"]} 47 + clap = { workspace = true, optional = true } 48 + 49 + [features] 50 + clap = ["dep:clap"] 45 51 46 52 [lints] 47 53 workspace = true
+47 -163
crates/atproto-record/src/bin/atproto-record-sign.rs
··· 7 7 }; 8 8 use atproto_record::signature::create; 9 9 use chrono::{SecondsFormat, Utc}; 10 + use clap::Parser; 10 11 use serde_json::json; 11 12 use std::{ 12 13 collections::HashMap, 13 - env, fs, 14 + fs, 14 15 io::{self, Read}, 15 16 }; 16 17 17 - /// AT Protocol Record Signing Tool 18 - /// 19 - /// This command-line tool provides cryptographic signing capabilities for AT Protocol records. 20 - /// It reads a JSON record from a file or stdin, applies a cryptographic signature using a DID key, 21 - /// and outputs the signed record with embedded signature metadata. 22 - /// 23 - /// ## Overview 24 - /// 25 - /// The tool performs the following operations: 26 - /// 1. **Command Line Parsing**: Extracts signing parameters from command line arguments 27 - /// 2. **Key Resolution**: Converts DID key strings to cryptographic key material 28 - /// 3. **Record Loading**: Reads and parses JSON records from disk files or stdin 29 - /// 4. **Signature Creation**: Generates cryptographic signatures using IPLD DAG-CBOR serialization 30 - /// 5. **Output Generation**: Produces signed records with embedded signature objects 31 - /// 32 - /// ## Signature Process 33 - /// 34 - /// The signing process follows AT Protocol conventions: 35 - /// - Creates a `$sig` object with repository and collection context 36 - /// - Serializes the record with `$sig` using IPLD DAG-CBOR format 37 - /// - Generates ECDSA signatures using P-256 or K-256 curves 38 - /// - Embeds signatures in a `signatures` array with issuer metadata 39 - /// - Encodes signatures using multibase (base64url) 40 - /// 41 - /// ## Arguments 42 - /// 43 - /// The tool accepts flexible argument ordering: 44 - /// - **DID Key** (`did:key:...`) - Cryptographic key for signing operations 45 - /// - **Issuer DID** (`did:plc:...` or `did:web:...`) - Identity of the signature issuer 46 - /// - **Record Input** (file path or `--`) - JSON file containing the record to sign, or `--` to read from stdin 47 - /// - **Parameters** (`key=value`) - Repository, collection, and signature metadata 48 - /// 49 - /// ## Required Parameters 50 - /// 51 - /// - `repository=<DID>` - Repository context for the signature 52 - /// - `collection=<name>` - Collection type context for the signature 53 - /// 54 - /// ## Optional Parameters 55 - /// 56 - /// - `issued_at=<timestamp>` - RFC 3339 timestamp (defaults to current time) 57 - /// - Custom fields can be added to the signature object via `key=value` pairs 58 - /// 59 - /// ## Examples 60 - /// 61 - /// ### Basic Usage 62 - /// ```bash 63 - /// atproto-record-sign \ 64 - /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 65 - /// ./post.json \ 66 - /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 67 - /// repository=did:plc:4zutorghlchjxzgceklue4la \ 68 - /// collection=app.bsky.feed.post 69 - /// ``` 70 - /// 71 - /// ### With Custom Timestamp 72 - /// ```bash 73 - /// atproto-record-sign \ 74 - /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 75 - /// ./badge.json \ 76 - /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 77 - /// repository=did:plc:4zutorghlchjxzgceklue4la \ 78 - /// collection=community.lexicon.badge.award \ 79 - /// issued_at=2025-05-16T14:00:02.000Z 80 - /// ``` 81 - /// 82 - /// ### Reading from Stdin 83 - /// ```bash 84 - /// echo '{"$type":"app.bsky.feed.post","text":"Hello from stdin!"}' | \ 85 - /// atproto-record-sign \ 86 - /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 87 - /// -- \ 88 - /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 89 - /// repository=did:plc:4zutorghlchjxzgceklue4la \ 90 - /// collection=app.bsky.feed.post 91 - /// ``` 92 - /// 93 - /// ### Input Record Example (`post.json`) 94 - /// ```json 95 - /// { 96 - /// "$type": "app.bsky.feed.post", 97 - /// "text": "Hello AT Protocol!", 98 - /// "createdAt": "2024-01-01T00:00:00Z" 99 - /// } 100 - /// ``` 101 - /// 102 - /// ### Output Signed Record 103 - /// ```json 104 - /// { 105 - /// "$type": "app.bsky.feed.post", 106 - /// "text": "Hello AT Protocol!", 107 - /// "createdAt": "2024-01-01T00:00:00Z", 108 - /// "signatures": [ 109 - /// { 110 - /// "issuer": "did:plc:tgudj2fjm77pzkuawquqhsxm", 111 - /// "issued_at": "2025-05-30T16:27:20.532Z", 112 - /// "signature": "uo36qFvSGV6QcFaxYYN9JCAGQNv2yVHK2LPN3lNp210v..." 113 - /// } 114 - /// ] 115 - /// } 116 - /// ``` 117 - /// 118 - /// ## Error Handling 119 - /// 120 - /// The tool provides detailed error messages for: 121 - /// - Missing or invalid DID keys and issuer DIDs 122 - /// - File reading and JSON parsing failures 123 - /// - Missing required parameters (repository, collection) 124 - /// - Cryptographic operation failures 125 - /// - Unsupported DID methods 126 - /// 127 - /// ## Security Considerations 128 - /// 129 - /// - Private keys are handled in-memory only and not persisted 130 - /// - Signatures are generated using industry-standard ECDSA algorithms 131 - /// - All cryptographic operations follow AT Protocol specifications 132 - /// - Input validation prevents malformed DID and parameter injection 18 + /// AT Protocol Record Signing CLI 19 + #[derive(Parser)] 20 + #[command( 21 + name = "atproto-record-sign", 22 + version, 23 + about = "Sign AT Protocol records with cryptographic signatures", 24 + long_about = " 25 + A command-line tool for signing AT Protocol records using DID keys. Reads a JSON 26 + record from a file or stdin, applies a cryptographic signature, and outputs the 27 + signed record with embedded signature metadata. 28 + 29 + The tool accepts flexible argument ordering with DID keys, issuer DIDs, record 30 + inputs, and key=value parameters for repository and collection context. 31 + 32 + REQUIRED PARAMETERS: 33 + repository=<DID> Repository context for the signature 34 + collection=<name> Collection type context for the signature 35 + 36 + EXAMPLES: 37 + # Basic usage: 38 + atproto-record-sign \\ 39 + did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\ 40 + ./post.json \\ 41 + did:plc:tgudj2fjm77pzkuawquqhsxm \\ 42 + repository=did:plc:4zutorghlchjxzgceklue4la \\ 43 + collection=app.bsky.feed.post 44 + 45 + # Reading from stdin: 46 + echo '{\"text\":\"Hello!\"}' | atproto-record-sign \\ 47 + did:key:z42tv1pb3... -- did:plc:issuer... \\ 48 + repository=did:plc:repo... collection=app.bsky.feed.post 49 + 50 + SIGNATURE PROCESS: 51 + - Creates $sig object with repository and collection context 52 + - Serializes record using IPLD DAG-CBOR format 53 + - Generates ECDSA signatures using P-256 or K-256 curves 54 + - Embeds signatures with issuer metadata 55 + " 56 + )] 57 + struct Args { 58 + /// All arguments - flexible parsing handles DID keys, issuer DIDs, files, and key=value pairs 59 + args: Vec<String>, 60 + } 133 61 #[tokio::main] 134 62 async fn main() -> Result<()> { 135 - // Check for help flags 136 - let args: Vec<String> = env::args().skip(1).collect(); 137 - if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") { 138 - println!("AT Protocol Record Signing Tool"); 139 - println!(); 140 - println!("USAGE:"); 141 - println!( 142 - " atproto-record-sign <ISSUER_DID> <SIGNING_KEY> <RECORD_INPUT> repository=<REPO> collection=<COLLECTION> [key=value...]" 143 - ); 144 - println!(); 145 - println!("ARGUMENTS:"); 146 - println!(" <ISSUER_DID> DID of the issuer (e.g., did:plc:...)"); 147 - println!(" <SIGNING_KEY> DID key for signing (e.g., did:key:z42tv1...)"); 148 - println!( 149 - " <RECORD_INPUT> Path to JSON file containing the record to sign, or '--' to read from stdin" 150 - ); 151 - println!(); 152 - println!("REQUIRED PARAMETERS:"); 153 - println!(" repository=<REPO> Repository DID context"); 154 - println!(" collection=<COLLECTION> Collection name context"); 155 - println!(); 156 - println!("OPTIONAL PARAMETERS:"); 157 - println!(" issued_at=<TIMESTAMP> RFC 3339 timestamp (defaults to current time)"); 158 - println!(" [key=value...] Additional fields for signature object"); 159 - println!(); 160 - println!("EXAMPLES:"); 161 - println!(" # Sign from file:"); 162 - println!(" atproto-record-sign \\"); 163 - println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\"); 164 - println!(" ./record.json \\"); 165 - println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\"); 166 - println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\"); 167 - println!(" collection=community.lexicon.badge.award"); 168 - println!(); 169 - println!(" # Sign from stdin:"); 170 - println!( 171 - " echo '{{\"$type\":\"app.bsky.feed.post\",\"text\":\"Hello!\"}}' | atproto-record-sign \\" 172 - ); 173 - println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\"); 174 - println!(" -- \\"); 175 - println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\"); 176 - println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\"); 177 - println!(" collection=app.bsky.feed.post"); 178 - return Ok(()); 179 - } 63 + let args = Args::parse(); 180 64 181 - let arguments = args.into_iter(); 65 + let arguments = args.args.into_iter(); 182 66 183 67 let mut collection: Option<String> = None; 184 68 let mut repository: Option<String> = None;
+47 -176
crates/atproto-record/src/bin/atproto-record-verify.rs
··· 6 6 resolve::{InputType, parse_input}, 7 7 }; 8 8 use atproto_record::signature::verify; 9 + use clap::Parser; 9 10 use std::{ 10 - env, fs, 11 + fs, 11 12 io::{self, Read}, 12 13 }; 13 14 14 - /// AT Protocol Record Verification Tool 15 - /// 16 - /// This command-line tool provides cryptographic signature verification capabilities for AT Protocol records. 17 - /// It reads a signed JSON record from a file or stdin, validates the embedded cryptographic signatures using a public key, 18 - /// and reports whether the signature verification succeeds or fails. 19 - /// 20 - /// ## Overview 21 - /// 22 - /// The tool performs the following operations: 23 - /// 1. **Command Line Parsing**: Extracts verification parameters from command line arguments 24 - /// 2. **Key Resolution**: Converts DID key strings to cryptographic key material for verification 25 - /// 3. **Record Loading**: Reads and parses signed JSON records from disk files or stdin 26 - /// 4. **Signature Verification**: Validates cryptographic signatures using IPLD DAG-CBOR deserialization 27 - /// 5. **Result Reporting**: Outputs verification success or detailed failure information 28 - /// 29 - /// ## Verification Process 30 - /// 31 - /// The verification process follows AT Protocol conventions: 32 - /// - Extracts signatures from the `signatures` array in the record 33 - /// - Finds signatures matching the specified issuer DID 34 - /// - Reconstructs the `$sig` object with repository and collection context 35 - /// - Deserializes the record with `$sig` using IPLD DAG-CBOR format 36 - /// - Validates ECDSA signatures using P-256 or K-256 curves 37 - /// - Decodes signatures from multibase (base64url) encoding 38 - /// - Verifies cryptographic signatures against the public key 39 - /// 40 - /// ## Arguments 41 - /// 42 - /// The tool accepts flexible argument ordering: 43 - /// - **Issuer DID** (`did:plc:...` or `did:web:...`) - Identity of the expected signature issuer 44 - /// - **Verification Key** (`did:key:...`) - Public key for signature verification 45 - /// - **Record Input** (file path or `--`) - JSON file containing the signed record to verify, or `--` to read from stdin 46 - /// - **Parameters** (`key=value`) - Repository and collection context for verification 47 - /// 48 - /// ## Required Parameters 49 - /// 50 - /// - `repository=<DID>` - Repository context that was used during signing 51 - /// - `collection=<name>` - Collection type context that was used during signing 52 - /// 53 - /// ## Examples 54 - /// 55 - /// ### Basic Verification 56 - /// ```bash 57 - /// atproto-record-verify \ 58 - /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 59 - /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 60 - /// ./signed_post.json \ 61 - /// repository=did:plc:4zutorghlchjxzgceklue4la \ 62 - /// collection=app.bsky.feed.post 63 - /// ``` 64 - /// 65 - /// ### Verifying Badge Awards 66 - /// ```bash 67 - /// atproto-record-verify \ 68 - /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 69 - /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 70 - /// ./signed_badge.json \ 71 - /// repository=did:plc:4zutorghlchjxzgceklue4la \ 72 - /// collection=community.lexicon.badge.award 73 - /// ``` 74 - /// 75 - /// ### Verifying from Stdin 76 - /// ```bash 77 - /// echo '{"signatures":[...],"text":"Hello!"}' | \ 78 - /// atproto-record-verify \ 79 - /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 80 - /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 81 - /// -- \ 82 - /// repository=did:plc:4zutorghlchjxzgceklue4la \ 83 - /// collection=app.bsky.feed.post 84 - /// ``` 85 - /// 86 - /// ### Input Signed Record Example (`signed_post.json`) 87 - /// ```json 88 - /// { 89 - /// "$type": "app.bsky.feed.post", 90 - /// "text": "Hello AT Protocol!", 91 - /// "createdAt": "2024-01-01T00:00:00Z", 92 - /// "signatures": [ 93 - /// { 94 - /// "issuer": "did:plc:tgudj2fjm77pzkuawquqhsxm", 95 - /// "issued_at": "2025-05-30T16:27:20.532Z", 96 - /// "signature": "uo36qFvSGV6QcFaxYYN9JCAGQNv2yVHK2LPN3lNp210v..." 97 - /// } 98 - /// ] 99 - /// } 100 - /// ``` 101 - /// 102 - /// ### Successful Verification Output 103 - /// ``` 104 - /// OK 105 - /// ``` 106 - /// 107 - /// ### Failed Verification Output 108 - /// ``` 109 - /// Error: Signature verification failed: error validating signature: ... 110 - /// ``` 111 - /// 112 - /// ## Verification Workflow 113 - /// 114 - /// 1. **Parse Arguments**: Extract issuer DID, verification key, record file, and context parameters 115 - /// 2. **Load Record**: Read and parse the signed JSON record from the specified file 116 - /// 3. **Extract Signatures**: Find signature objects in the `signatures` array matching the issuer 117 - /// 4. **Reconstruct Context**: Recreate the `$sig` object with repository and collection context 118 - /// 5. **Serialize Content**: Convert the record with `$sig` to IPLD DAG-CBOR format 119 - /// 6. **Decode Signature**: Convert multibase-encoded signature to raw bytes 120 - /// 7. **Verify Cryptographically**: Validate the signature against the serialized content using the public key 121 - /// 8. **Report Result**: Output "OK" for valid signatures or detailed error messages for failures 122 - /// 123 - /// ## Error Handling 124 - /// 125 - /// The tool provides detailed error messages for: 126 - /// - Missing or invalid DID keys and issuer DIDs 127 - /// - File reading and JSON parsing failures 128 - /// - Missing required parameters (repository, collection) 129 - /// - Records without signature fields or matching issuer signatures 130 - /// - Signature decoding and deserialization failures 131 - /// - Cryptographic verification failures 132 - /// - Unsupported DID methods and malformed signatures 133 - /// 134 - /// ## Security Considerations 135 - /// 136 - /// - Public keys are used for verification only, no private key handling 137 - /// - Signature verification uses industry-standard ECDSA algorithms 138 - /// - All cryptographic operations follow AT Protocol specifications 139 - /// - Input validation prevents malformed DID and parameter injection 140 - /// - Failed verifications provide diagnostic information without exposing sensitive data 141 - /// 142 - /// ## Integration with Signing Tool 143 - /// 144 - /// This tool is designed to work with records produced by `atproto-record-sign`: 145 - /// 1. Use `atproto-record-sign` to create signed records with embedded signatures 146 - /// 2. Use `atproto-record-verify` to validate those signatures using the corresponding public key 147 - /// 3. The same repository and collection parameters must be used for both signing and verification 148 - /// 4. The verification key should correspond to the private key used for signing 15 + /// AT Protocol Record Verification CLI 16 + #[derive(Parser)] 17 + #[command( 18 + name = "atproto-record-verify", 19 + version, 20 + about = "Verify cryptographic signatures of AT Protocol records", 21 + long_about = " 22 + A command-line tool for verifying cryptographic signatures of AT Protocol records. 23 + Reads a signed JSON record from a file or stdin, validates the embedded signatures 24 + using a public key, and reports verification success or failure. 25 + 26 + The tool accepts flexible argument ordering with issuer DIDs, verification keys, 27 + record inputs, and key=value parameters for repository and collection context. 28 + 29 + REQUIRED PARAMETERS: 30 + repository=<DID> Repository context used during signing 31 + collection=<name> Collection type context used during signing 32 + 33 + EXAMPLES: 34 + # Basic verification: 35 + atproto-record-verify \\ 36 + did:plc:tgudj2fjm77pzkuawquqhsxm \\ 37 + did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\ 38 + ./signed_post.json \\ 39 + repository=did:plc:4zutorghlchjxzgceklue4la \\ 40 + collection=app.bsky.feed.post 41 + 42 + # Verify from stdin: 43 + echo '{\"signatures\":[...]}' | atproto-record-verify \\ 44 + did:plc:issuer... did:key:z42tv1pb3... -- \\ 45 + repository=did:plc:repo... collection=app.bsky.feed.post 46 + 47 + VERIFICATION PROCESS: 48 + - Extracts signatures from the signatures array 49 + - Finds signatures matching the specified issuer DID 50 + - Reconstructs $sig object with repository and collection context 51 + - Validates ECDSA signatures using P-256 or K-256 curves 52 + " 53 + )] 54 + struct Args { 55 + /// All arguments - flexible parsing handles issuer DIDs, verification keys, files, and key=value pairs 56 + args: Vec<String>, 57 + } 149 58 #[tokio::main] 150 59 async fn main() -> Result<()> { 151 - // Check for help flags 152 - let args: Vec<String> = env::args().skip(1).collect(); 153 - if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") { 154 - println!("AT Protocol Record Verifying Tool"); 155 - println!(); 156 - println!("USAGE:"); 157 - println!( 158 - " atproto-record-verify <ISSUER_DID> <KEY> <RECORD_INPUT> repository=<REPO> collection=<COLLECTION> [key=value...]" 159 - ); 160 - println!(); 161 - println!("ARGUMENTS:"); 162 - println!(" <ISSUER_DID> DID of the issuer (e.g., did:plc:...)"); 163 - println!(" <KEY> DID key for verifying (e.g., did:key:z42tv1...)"); 164 - println!( 165 - " <RECORD_INPUT> Path to JSON file containing the record to verify, or '--' to read from stdin" 166 - ); 167 - println!(); 168 - println!("REQUIRED PARAMETERS:"); 169 - println!(" repository=<REPO> Repository DID context"); 170 - println!(" collection=<COLLECTION> Collection name context"); 171 - println!(); 172 - println!("EXAMPLES:"); 173 - println!(" # Verify from file:"); 174 - println!(" atproto-record-verify \\"); 175 - println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\"); 176 - println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\"); 177 - println!(" ./signed_record.json \\"); 178 - println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\"); 179 - println!(" collection=community.lexicon.badge.award"); 180 - println!(); 181 - println!(" # Verify from stdin:"); 182 - println!(" echo '{{\"signatures\":[...],...}}' | atproto-record-verify \\"); 183 - println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\"); 184 - println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\"); 185 - println!(" -- \\"); 186 - println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\"); 187 - println!(" collection=app.bsky.feed.post"); 188 - return Ok(()); 189 - } 60 + let args = Args::parse(); 190 61 191 - let arguments = args.into_iter(); 62 + let arguments = args.args.into_iter(); 192 63 193 64 let mut collection: Option<String> = None; 194 65 let mut repository: Option<String> = None;
+12
crates/atproto-xrpcs-helloworld/Cargo.toml
··· 10 10 keywords.workspace = true 11 11 categories.workspace = true 12 12 13 + [[bin]] 14 + name = "atproto-xrpcs-helloworld" 15 + path = "src/main.rs" 16 + test = false 17 + bench = false 18 + doc = true 19 + required-features = ["clap"] 20 + 13 21 [dependencies] 14 22 atproto-identity.workspace = true 15 23 atproto-record.workspace = true ··· 33 41 urlencoding = "2.1.3" 34 42 axum = { version = "0.8", features = ["macros"] } 35 43 http = "1.0.0" 44 + clap = { workspace = true, optional = true } 45 + 46 + [features] 47 + clap = ["dep:clap"] 36 48 37 49 [lints] 38 50 workspace = true
+37 -13
crates/atproto-xrpcs-helloworld/src/main.rs
··· 38 38 response::{Html, IntoResponse, Response}, 39 39 routing::get, 40 40 }; 41 + use clap::Parser; 41 42 use http::{HeaderMap, StatusCode, request::Parts}; 42 43 use serde::Deserialize; 43 44 use serde_json::json; 44 - use std::{ 45 - collections::HashMap, convert::Infallible, env, num::NonZeroUsize, ops::Deref, sync::Arc, 46 - }; 45 + use std::{collections::HashMap, convert::Infallible, num::NonZeroUsize, ops::Deref, sync::Arc}; 47 46 48 47 #[derive(Clone)] 49 48 pub struct SimpleKeyProvider { ··· 161 160 } 162 161 } 163 162 164 - fn print_usage() { 165 - println!("Hello World XRPC Service"); 166 - } 163 + /// AT Protocol XRPC Hello World Service 164 + #[derive(Parser)] 165 + #[command( 166 + name = "atproto-xrpcs-helloworld", 167 + version, 168 + about = "AT Protocol XRPC Hello World demonstration service", 169 + long_about = " 170 + A demonstration XRPC service implementation showcasing the AT Protocol ecosystem. 171 + This service provides a simple \"Hello, World!\" endpoint that supports both 172 + authenticated and unauthenticated requests. 173 + 174 + FEATURES: 175 + - AT Protocol identity resolution and DID document management 176 + - XRPC service endpoint with optional authentication 177 + - DID:web identity publishing via .well-known endpoints 178 + - JWT-based request authentication using AT Protocol standards 179 + 180 + ENVIRONMENT VARIABLES: 181 + SERVICE_KEY Private key for service identity (required) 182 + EXTERNAL_BASE External hostname for service endpoints (required) 183 + PORT HTTP server port (default: 8080) 184 + PLC_HOSTNAME PLC directory hostname (default: plc.directory) 185 + USER_AGENT HTTP User-Agent header (auto-generated) 186 + DNS_NAMESERVERS Custom DNS nameservers (optional) 187 + CERTIFICATE_BUNDLES Additional CA certificates (optional) 188 + 189 + ENDPOINTS: 190 + GET / HTML index page 191 + GET /.well-known/did.json DID document (DID:web) 192 + GET /.well-known/atproto-did AT Protocol DID identifier 193 + GET /xrpc/.../Hello Hello World XRPC endpoint 194 + " 195 + )] 196 + struct Args {} 167 197 168 198 #[tokio::main] 169 199 async fn main() -> Result<()> { 170 - // Parse command line arguments 171 - let args: Vec<String> = env::args().skip(1).collect(); 172 - 173 - if args.iter().any(|arg| arg == "--help" || arg == "-h") { 174 - print_usage(); 175 - return Ok(()); 176 - } 200 + let _args = Args::parse(); 177 201 178 202 let plc_hostname = default_env("PLC_HOSTNAME", "plc.directory"); 179 203 let certificate_bundles: CertificateBundles = optional_env("CERTIFICATE_BUNDLES").try_into()?;