A library for ATProtocol identities.

feature: Added stdin support to binaries.

* The atproto-record-sign tool can read records from stdin.
* The atproto-record-verify tool can read records from stdin.
* Added a top-level project README.md file.
* Added missing LICENSE file.

Changed files
+325 -19
crates
+2
CLAUDE.prompts.md
··· 24 24 25 25 Write a project `README.md` file that describes the project as a library that supports ATProtocol identity record signing and verifying. Note that parts of this was extracted from the open sourced https://tangled.sh/@smokesignal.events/smokesignal project. This project is open source under the MIT license. 26 26 27 + The `REAADME.md` file should provide a high level overview of both the `atproto-identity` and `atproto-record` crates. It should also concisely reference the available binaries and provide a minimal example of how to use them. 28 + 27 29 ## Check and clippy 28 30 29 31 Using `cargo clippy`, satisfy warnings. Think very hard about how to do this.
+9
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2024 Nick Gerakines 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 + 7 + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+220
README.md
··· 1 + # AT Protocol Identity & Record Library 2 + 3 + A comprehensive Rust library for AT Protocol identity management and record signing/verification. This library provides full functionality for DID resolution, handle resolution, identity document management, and cryptographic record operations across multiple DID methods. 4 + 5 + Parts of this library were extracted from the open-sourced [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project. 6 + 7 + ## Crates 8 + 9 + This workspace contains two main crates: 10 + 11 + ### `atproto-identity` 12 + 13 + A comprehensive AT Protocol identity management library providing: 14 + 15 + - **DID Resolution**: Support for `did:plc`, `did:web`, and `did:key` methods 16 + - **Handle Resolution**: DNS-based handle resolution with validation 17 + - **Identity Documents**: Complete DID document parsing and management 18 + - **Cryptographic Operations**: P-256 and K-256 elliptic curve support 19 + - **Validation**: Input validation for handles and DIDs 20 + - **Configuration**: Environment-based configuration management 21 + 22 + ### `atproto-record` 23 + 24 + AT Protocol record signature operations library providing: 25 + 26 + - **Record Signing**: Create cryptographic signatures for AT Protocol records 27 + - **Signature Verification**: Verify existing signatures against records and public keys 28 + - **IPLD Integration**: Proper IPLD DAG-CBOR serialization for signature content 29 + - **Multi-curve Support**: Support for P-256 and K-256 elliptic curves 30 + 31 + ## CLI Tools 32 + 33 + The library includes several command-line utilities: 34 + 35 + ### atproto-identity 36 + 37 + - `atproto-identity-resolve` - Resolve DIDs and handles to identity documents 38 + - `atproto-identity-sign` - Sign identity-related operations 39 + - `atproto-identity-validate` - Validate DID and handle formats 40 + 41 + ### atproto-record 42 + 43 + - `atproto-record-sign` - Sign AT Protocol records from files or stdin 44 + - `atproto-record-verify` - Verify AT Protocol record signatures from files or stdin 45 + 46 + ## Quick Start 47 + 48 + Add the crates to your `Cargo.toml`: 49 + 50 + ```toml 51 + [dependencies] 52 + atproto-identity = "0.3.0" 53 + atproto-record = "0.3.0" 54 + ``` 55 + 56 + ### Basic Identity Resolution 57 + 58 + ```rust 59 + use atproto_identity::resolve::resolve_handle; 60 + 61 + #[tokio::main] 62 + async fn main() -> anyhow::Result<()> { 63 + // Resolve a handle to a DID 64 + let did = resolve_handle("alice.bsky.social").await?; 65 + println!("Resolved DID: {}", did); 66 + 67 + Ok(()) 68 + } 69 + ``` 70 + 71 + ### Record Signing and Verification 72 + 73 + ```rust 74 + use atproto_identity::key::identify_key; 75 + use atproto_record::signature; 76 + use serde_json::json; 77 + 78 + #[tokio::main] 79 + async fn main() -> anyhow::Result<()> { 80 + // Parse DID key for signing operations 81 + let signing_key = identify_key("did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW")?; 82 + 83 + // Create a record to sign 84 + let record = json!({ 85 + "$type": "app.bsky.feed.post", 86 + "text": "Hello AT Protocol!", 87 + "createdAt": "2024-01-01T00:00:00.000Z" 88 + }); 89 + 90 + // Create signature object with issuer and timestamp 91 + let signature_object = json!({ 92 + "issuer": "did:plc:tgudj2fjm77pzkuawquqhsxm", 93 + "issued_at": "2024-01-01T00:00:00.000Z" 94 + }); 95 + 96 + // Sign the record 97 + let signed_record = signature::create( 98 + &signing_key, 99 + &record, 100 + "did:plc:4zutorghlchjxzgceklue4la", // repository 101 + "app.bsky.feed.post", // collection 102 + signature_object, 103 + ).await?; 104 + 105 + // Verify the signature 106 + signature::verify( 107 + "did:plc:tgudj2fjm77pzkuawquqhsxm", // issuer 108 + &signing_key, // verification key 109 + signed_record, // signed record 110 + "did:plc:4zutorghlchjxzgceklue4la", // repository 111 + "app.bsky.feed.post", // collection 112 + ).await?; 113 + 114 + println!("Signature verification successful"); 115 + 116 + Ok(()) 117 + } 118 + ``` 119 + 120 + ### CLI Usage Examples 121 + 122 + ```bash 123 + # Resolve a handle or DID 124 + cargo run --bin atproto-identity-resolve -- alice.bsky.social 125 + 126 + # Get full DID document 127 + cargo run --bin atproto-identity-resolve -- --did-document did:plc:abc123 128 + 129 + # Sign a record from file 130 + cargo run --bin atproto-record-sign -- \ 131 + did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 132 + ./record.json \ 133 + did:plc:tgudj2fjm77pzkuawquqhsxm \ 134 + repository=did:plc:4zutorghlchjxzgceklue4la \ 135 + collection=app.bsky.feed.post 136 + 137 + # Sign a record from stdin 138 + echo '{"$type":"app.bsky.feed.post","text":"Hello!"}' | \ 139 + cargo run --bin atproto-record-sign -- \ 140 + did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 141 + -- \ 142 + did:plc:tgudj2fjm77pzkuawquqhsxm \ 143 + repository=did:plc:4zutorghlchjxzgceklue4la \ 144 + collection=app.bsky.feed.post 145 + 146 + # Verify a signature from file 147 + cargo run --bin atproto-record-verify -- \ 148 + did:plc:tgudj2fjm77pzkuawquqhsxm \ 149 + did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 150 + ./signed_record.json \ 151 + repository=did:plc:4zutorghlchjxzgceklue4la \ 152 + collection=app.bsky.feed.post 153 + 154 + # Verify a signature from stdin 155 + echo '{"signatures":[...],"text":"Hello!"}' | \ 156 + cargo run --bin atproto-record-verify -- \ 157 + did:plc:tgudj2fjm77pzkuawquqhsxm \ 158 + did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 159 + -- \ 160 + repository=did:plc:4zutorghlchjxzgceklue4la \ 161 + collection=app.bsky.feed.post 162 + ``` 163 + 164 + ## Development 165 + 166 + ### Building 167 + 168 + ```bash 169 + cargo build 170 + ``` 171 + 172 + ### Running Tests 173 + 174 + ```bash 175 + cargo test 176 + ``` 177 + 178 + ### Code Quality 179 + 180 + ```bash 181 + # Format code 182 + cargo fmt 183 + 184 + # Lint 185 + cargo clippy 186 + 187 + # Check without building 188 + cargo check 189 + ``` 190 + 191 + ## Features 192 + 193 + - **Async/Await**: Built with modern Rust async patterns using Tokio 194 + - **Error Handling**: Comprehensive structured error types using `thiserror` 195 + - **Logging**: Structured logging with `tracing` 196 + - **Security**: Forbids unsafe code and follows security best practices 197 + - **Standards Compliance**: Full AT Protocol specification compliance 198 + - **Multi-platform**: Works on all major platforms 199 + 200 + ## Architecture 201 + 202 + The library follows a modular architecture with clear separation of concerns: 203 + 204 + - **Identity Management**: Handle DID resolution, validation, and document management 205 + - **Cryptographic Operations**: Secure key operations and signature handling 206 + - **Network Operations**: HTTP and DNS resolution with proper error handling 207 + - **Data Models**: Comprehensive type definitions for AT Protocol entities 208 + - **CLI Tools**: Ready-to-use command-line utilities 209 + 210 + ## License 211 + 212 + MIT License - see [LICENSE](LICENSE) for details. 213 + 214 + ## Contributing 215 + 216 + Contributions are welcome! This project follows standard Rust conventions and includes comprehensive testing and documentation requirements. 217 + 218 + ## Repository 219 + 220 + https://tangled.sh/@smokesignal.events/atproto-identity-rs
+47 -9
crates/atproto-record/src/bin/atproto-record-sign.rs
··· 6 6 use atproto_record::signature::create; 7 7 use chrono::{SecondsFormat, Utc}; 8 8 use serde_json::json; 9 - use std::{collections::HashMap, env, fs}; 9 + use std::{ 10 + collections::HashMap, 11 + env, fs, 12 + io::{self, Read}, 13 + }; 10 14 11 15 /// AT Protocol Record Signing Tool 12 16 /// 13 17 /// This command-line tool provides cryptographic signing capabilities for AT Protocol records. 14 - /// It reads a JSON record from a file, applies a cryptographic signature using a DID key, 18 + /// It reads a JSON record from a file or stdin, applies a cryptographic signature using a DID key, 15 19 /// and outputs the signed record with embedded signature metadata. 16 20 /// 17 21 /// ## Overview ··· 19 23 /// The tool performs the following operations: 20 24 /// 1. **Command Line Parsing**: Extracts signing parameters from command line arguments 21 25 /// 2. **Key Resolution**: Converts DID key strings to cryptographic key material 22 - /// 3. **Record Loading**: Reads and parses JSON records from disk files 26 + /// 3. **Record Loading**: Reads and parses JSON records from disk files or stdin 23 27 /// 4. **Signature Creation**: Generates cryptographic signatures using IPLD DAG-CBOR serialization 24 28 /// 5. **Output Generation**: Produces signed records with embedded signature objects 25 29 /// ··· 37 41 /// The tool accepts flexible argument ordering: 38 42 /// - **DID Key** (`did:key:...`) - Cryptographic key for signing operations 39 43 /// - **Issuer DID** (`did:plc:...` or `did:web:...`) - Identity of the signature issuer 40 - /// - **Record File** (file path) - JSON file containing the record to sign 44 + /// - **Record Input** (file path or `--`) - JSON file containing the record to sign, or `--` to read from stdin 41 45 /// - **Parameters** (`key=value`) - Repository, collection, and signature metadata 42 46 /// 43 47 /// ## Required Parameters ··· 73 77 /// issued_at=2025-05-16T14:00:02.000Z 74 78 /// ``` 75 79 /// 80 + /// ### Reading from Stdin 81 + /// ```bash 82 + /// echo '{"$type":"app.bsky.feed.post","text":"Hello from stdin!"}' | \ 83 + /// atproto-record-sign \ 84 + /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 85 + /// -- \ 86 + /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 87 + /// repository=did:plc:4zutorghlchjxzgceklue4la \ 88 + /// collection=app.bsky.feed.post 89 + /// ``` 90 + /// 76 91 /// ### Input Record Example (`post.json`) 77 92 /// ```json 78 93 /// { ··· 121 136 println!("AT Protocol Record Signing Tool"); 122 137 println!(); 123 138 println!("USAGE:"); 124 - println!(" atproto-record-sign <ISSUER_DID> <SIGNING_KEY> <RECORD_FILE> repository=<REPO> collection=<COLLECTION> [key=value...]"); 139 + println!(" atproto-record-sign <ISSUER_DID> <SIGNING_KEY> <RECORD_INPUT> repository=<REPO> collection=<COLLECTION> [key=value...]"); 125 140 println!(); 126 141 println!("ARGUMENTS:"); 127 - println!(" <ISSUER_DID> DID of the issuer (e.g., did:plc:...)"); 128 - println!(" <SIGNING_KEY> DID key for signing (e.g., did:key:z42tv1...)"); 129 - println!(" <RECORD_FILE> Path to JSON file containing the record to sign"); 142 + println!(" <ISSUER_DID> DID of the issuer (e.g., did:plc:...)"); 143 + println!(" <SIGNING_KEY> DID key for signing (e.g., did:key:z42tv1...)"); 144 + println!(" <RECORD_INPUT> Path to JSON file containing the record to sign, or '--' to read from stdin"); 130 145 println!(); 131 146 println!("REQUIRED PARAMETERS:"); 132 147 println!(" repository=<REPO> Repository DID context"); ··· 136 151 println!(" issued_at=<TIMESTAMP> RFC 3339 timestamp (defaults to current time)"); 137 152 println!(" [key=value...] Additional fields for signature object"); 138 153 println!(); 139 - println!("EXAMPLE:"); 154 + println!("EXAMPLES:"); 155 + println!(" # Sign from file:"); 140 156 println!(" atproto-record-sign \\"); 141 157 println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\"); 142 158 println!(" ./record.json \\"); 143 159 println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\"); 144 160 println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\"); 145 161 println!(" collection=community.lexicon.badge.award"); 162 + println!(); 163 + println!(" # Sign from stdin:"); 164 + println!(" echo '{{\"$type\":\"app.bsky.feed.post\",\"text\":\"Hello!\"}}' | atproto-record-sign \\"); 165 + println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\"); 166 + println!(" -- \\"); 167 + println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\"); 168 + println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\"); 169 + println!(" collection=app.bsky.feed.post"); 146 170 return Ok(()); 147 171 } 148 172 ··· 178 202 } 179 203 Ok(_) => return Err(anyhow!("Unsupported DID method: {}", argument)), 180 204 Err(e) => return Err(anyhow!("Failed to parse DID {}: {}", argument, e)), 205 + } 206 + } else if argument == "--" { 207 + // Read record from stdin 208 + if record.is_none() { 209 + let mut stdin_content = String::new(); 210 + io::stdin() 211 + .read_to_string(&mut stdin_content) 212 + .map_err(|e| anyhow!("Failed to read from stdin: {}", e))?; 213 + record = Some( 214 + serde_json::from_str(&stdin_content) 215 + .map_err(|e| anyhow!("Failed to parse JSON from stdin: {}", e))?, 216 + ); 217 + } else { 218 + return Err(anyhow!("Unexpected argument: {}", argument)); 181 219 } 182 220 } else { 183 221 // Assume it's a file path to read the record from
+47 -10
crates/atproto-record/src/bin/atproto-record-verify.rs
··· 4 4 resolve::{parse_input, InputType}, 5 5 }; 6 6 use atproto_record::signature::verify; 7 - use std::{env, fs}; 7 + use std::{ 8 + env, fs, 9 + io::{self, Read}, 10 + }; 8 11 9 12 /// AT Protocol Record Verification Tool 10 13 /// 11 14 /// This command-line tool provides cryptographic signature verification capabilities for AT Protocol records. 12 - /// It reads a signed JSON record from a file, validates the embedded cryptographic signatures using a public key, 15 + /// It reads a signed JSON record from a file or stdin, validates the embedded cryptographic signatures using a public key, 13 16 /// and reports whether the signature verification succeeds or fails. 14 17 /// 15 18 /// ## Overview ··· 17 20 /// The tool performs the following operations: 18 21 /// 1. **Command Line Parsing**: Extracts verification parameters from command line arguments 19 22 /// 2. **Key Resolution**: Converts DID key strings to cryptographic key material for verification 20 - /// 3. **Record Loading**: Reads and parses signed JSON records from disk files 23 + /// 3. **Record Loading**: Reads and parses signed JSON records from disk files or stdin 21 24 /// 4. **Signature Verification**: Validates cryptographic signatures using IPLD DAG-CBOR deserialization 22 25 /// 5. **Result Reporting**: Outputs verification success or detailed failure information 23 26 /// ··· 37 40 /// The tool accepts flexible argument ordering: 38 41 /// - **Issuer DID** (`did:plc:...` or `did:web:...`) - Identity of the expected signature issuer 39 42 /// - **Verification Key** (`did:key:...`) - Public key for signature verification 40 - /// - **Record File** (file path) - JSON file containing the signed record to verify 43 + /// - **Record Input** (file path or `--`) - JSON file containing the signed record to verify, or `--` to read from stdin 41 44 /// - **Parameters** (`key=value`) - Repository and collection context for verification 42 45 /// 43 46 /// ## Required Parameters ··· 67 70 /// collection=community.lexicon.badge.award 68 71 /// ``` 69 72 /// 73 + /// ### Verifying from Stdin 74 + /// ```bash 75 + /// echo '{"signatures":[...],"text":"Hello!"}' | \ 76 + /// atproto-record-verify \ 77 + /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 78 + /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 79 + /// -- \ 80 + /// repository=did:plc:4zutorghlchjxzgceklue4la \ 81 + /// collection=app.bsky.feed.post 82 + /// ``` 83 + /// 70 84 /// ### Input Signed Record Example (`signed_post.json`) 71 85 /// ```json 72 86 /// { ··· 138 152 println!("AT Protocol Record Verifying Tool"); 139 153 println!(); 140 154 println!("USAGE:"); 141 - println!(" atproto-record-verify <ISSUER_DID> <KEY> <RECORD_FILE> repository=<REPO> collection=<COLLECTION> [key=value...]"); 155 + println!(" atproto-record-verify <ISSUER_DID> <KEY> <RECORD_INPUT> repository=<REPO> collection=<COLLECTION> [key=value...]"); 142 156 println!(); 143 157 println!("ARGUMENTS:"); 144 - println!(" <ISSUER_DID> DID of the issuer (e.g., did:plc:...)"); 145 - println!(" <KEY> DID key for verifying (e.g., did:key:z42tv1...)"); 146 - println!(" <RECORD_FILE> Path to JSON file containing the record to verify"); 158 + println!(" <ISSUER_DID> DID of the issuer (e.g., did:plc:...)"); 159 + println!(" <KEY> DID key for verifying (e.g., did:key:z42tv1...)"); 160 + println!(" <RECORD_INPUT> Path to JSON file containing the record to verify, or '--' to read from stdin"); 147 161 println!(); 148 162 println!("REQUIRED PARAMETERS:"); 149 163 println!(" repository=<REPO> Repository DID context"); 150 164 println!(" collection=<COLLECTION> Collection name context"); 151 165 println!(); 152 - println!("EXAMPLE:"); 166 + println!("EXAMPLES:"); 167 + println!(" # Verify from file:"); 153 168 println!(" atproto-record-verify \\"); 154 169 println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\"); 155 170 println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\"); 156 - println!(" ./record.json \\"); 171 + println!(" ./signed_record.json \\"); 157 172 println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\"); 158 173 println!(" collection=community.lexicon.badge.award"); 174 + println!(); 175 + println!(" # Verify from stdin:"); 176 + println!(" echo '{{\"signatures\":[...],...}}' | atproto-record-verify \\"); 177 + println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\"); 178 + println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\"); 179 + println!(" -- \\"); 180 + println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\"); 181 + println!(" collection=app.bsky.feed.post"); 159 182 return Ok(()); 160 183 } 161 184 ··· 188 211 } 189 212 Ok(_) => return Err(anyhow!("Unsupported DID method: {}", argument)), 190 213 Err(e) => return Err(anyhow!("Failed to parse DID {}: {}", argument, e)), 214 + } 215 + } else if argument == "--" { 216 + // Read record from stdin 217 + if record.is_none() { 218 + let mut stdin_content = String::new(); 219 + io::stdin() 220 + .read_to_string(&mut stdin_content) 221 + .map_err(|e| anyhow!("Failed to read from stdin: {}", e))?; 222 + record = Some( 223 + serde_json::from_str(&stdin_content) 224 + .map_err(|e| anyhow!("Failed to parse JSON from stdin: {}", e))?, 225 + ); 226 + } else { 227 + return Err(anyhow!("Unexpected argument: {}", argument)); 191 228 } 192 229 } else { 193 230 // Assume it's a file path to read the record from