this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: restructure project into modular workspace architecture │

Split monolithic library into focused Cargo workspace with two specialized crates:
- atproto-identity: DID resolution, handle resolution, and identity management
- atproto-record: Record signature operations and cryptographic validation

+1400 -226
+29
CLAUDE.prompts.md
··· 1 + # Useful Prompts 2 + 3 + ## Find unecessary project dependencies 4 + 5 + Analyze all of the project dependencies and identify any that are not necessary or could be reduced. Think very very hard. 6 + 7 + ## Review and ensure error correctness 8 + 9 + Review all of the errors defined in `path/to/errors.rs` and ensure their names, messages, documentation, and usage are correct. Each error must have a globally unique identifier and error numbers must be ordered consistently. Think very very hard. 10 + 11 + ## Add missing documentation 12 + 13 + Using `cargo check`, add documentation to things that are missing documentation to satisfy compiler warnings. 14 + 15 + Document the `crates/atproto-record/src/bin/atproto-record-verify.rs` source file main method. It should include a high level overview of what the binary does. It should also include example usage. Think very hard. 16 + 17 + ## Review and ensure README correctness 18 + 19 + In the `atproto-record` crate, update project documentation and README files to ensure they are accurate and reflect current source code. Think very very hard. 20 + 21 + ## Module documentation 22 + 23 + Write high level module documentation in the `path/to/file.rs` source file. Documentation should brief and specific. Think very hard about how to do this. 24 + 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 + 27 + ## Check and clippy 28 + 29 + Using `cargo clippy`, satisfy warnings. Think very hard about how to do this.
+38 -1
Cargo.lock
··· 51 51 52 52 [[package]] 53 53 name = "atproto-identity" 54 - version = "0.2.0" 54 + version = "0.3.0" 55 55 dependencies = [ 56 56 "anyhow", 57 57 "ecdsa", ··· 60 60 "multibase", 61 61 "p256", 62 62 "reqwest", 63 + "serde", 64 + "serde_ipld_dagcbor", 65 + "serde_json", 66 + "thiserror 2.0.12", 67 + "tokio", 68 + "tracing", 69 + ] 70 + 71 + [[package]] 72 + name = "atproto-record" 73 + version = "0.3.0" 74 + dependencies = [ 75 + "anyhow", 76 + "atproto-identity", 77 + "chrono", 78 + "ecdsa", 79 + "k256", 80 + "multibase", 81 + "p256", 63 82 "serde", 64 83 "serde_ipld_dagcbor", 65 84 "serde_json", ··· 169 188 version = "0.2.1" 170 189 source = "registry+https://github.com/rust-lang/crates.io-index" 171 190 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 191 + 192 + [[package]] 193 + name = "chrono" 194 + version = "0.4.41" 195 + source = "registry+https://github.com/rust-lang/crates.io-index" 196 + checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 197 + dependencies = [ 198 + "num-traits", 199 + ] 172 200 173 201 [[package]] 174 202 name = "cid" ··· 1125 1153 dependencies = [ 1126 1154 "overload", 1127 1155 "winapi", 1156 + ] 1157 + 1158 + [[package]] 1159 + name = "num-traits" 1160 + version = "0.2.19" 1161 + source = "registry+https://github.com/rust-lang/crates.io-index" 1162 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1163 + dependencies = [ 1164 + "autocfg", 1128 1165 ] 1129 1166 1130 1167 [[package]]
+15 -25
Cargo.toml
··· 1 - [package] 2 - name = "atproto-identity" 3 - version = "0.2.0" 1 + [workspace] 2 + members = [ 3 + "crates/atproto-record", 4 + "crates/atproto-identity", 5 + ] 6 + resolver = "3" 7 + 8 + [workspace.package] 4 9 edition = "2021" 5 10 rust-version = "1.83" 11 + repository = "https://tangled.sh/@smokesignal.events/atproto-identity-rs" 6 12 authors = ["Nick Gerakines <nick.gerakines@gmail.com>"] 7 - description = "An ATProtocol identity library" 8 - readme = "README.md" 9 - repository = "https://tangled.sh/@smokesignal.events/atproto-identity-rs" 10 13 license = "MIT" 11 14 keywords = ["atprotocol"] 12 15 categories = ["command-line-utilities", "web-programming"] 13 - exclude = ["CLAUDE.md"] 14 16 15 - [[bin]] 16 - name = "atproto-identity-resolve" 17 - test = false 18 - bench = false 19 - doc = true 20 - 21 - [[bin]] 22 - name = "atproto-identity-sign" 23 - test = false 24 - bench = false 25 - doc = true 26 - 27 - [[bin]] 28 - name = "atproto-identity-validate" 29 - test = false 30 - bench = false 31 - doc = true 17 + [workspace.dependencies] 18 + atproto-identity = { version = "0.3.0", path = "crates/atproto-identity" } 19 + atproto-record = { version = "0.3.0", path = "crates/atproto-record" } 32 20 33 - [dependencies] 34 21 anyhow = "1.0" 35 22 serde = { version = "1.0", features = ["derive"] } 36 23 serde_json = "1.0" ··· 44 31 k256 = "0.13.4" 45 32 p256 = "0.13.2" 46 33 serde_ipld_dagcbor = "0.6.3" 34 + 35 + [workspace.lints.rust] 36 + unsafe_code = "forbid"
-199
README.md
··· 1 - # atproto-identity 2 - 3 - A Rust library for AT Protocol identity resolution and management. 4 - 5 - ## Overview 6 - 7 - `atproto-identity` provides comprehensive support for resolving and managing identities in the AT Protocol ecosystem. This library handles multiple DID (Decentralized Identifier) methods including `did:plc` and `did:web`, as well as AT Protocol handle resolution via both DNS and HTTP methods. 8 - 9 - This project was extracted from the open-sourced [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project and is designed to be a standalone, reusable library for AT Protocol identity operations. 10 - 11 - ## Features 12 - 13 - - **Handle Resolution**: Resolve AT Protocol handles to DIDs using DNS TXT records and HTTP well-known endpoints 14 - - **DID Document Retrieval**: Fetch and parse DID documents for `did:plc` and `did:web` identifiers 15 - - **Multiple Resolution Methods**: Supports both DNS and HTTP-based handle resolution with conflict detection 16 - - **Configurable DNS**: Custom DNS nameserver support with fallback to system defaults 17 - - **Cryptographic Key Operations**: Support for P-256 and K-256 key identification, signature validation, and signing 18 - - **Structured Logging**: Built-in tracing support for debugging and monitoring 19 - - **Type Safety**: Comprehensive error handling with structured error types 20 - 21 - ## Supported DID Methods 22 - 23 - - **did-method-plc**: Public Ledger of Credentials DIDs via PLC directory 24 - - **did-method-web**: Web-based DIDs following the did:web specification with URL conversion utilities 25 - - **ATProtocol Handle Resolution**: AT Protocol handles (e.g., `ngerakines.me`) can be resolved to DIDs 26 - 27 - ## Installation 28 - 29 - Add this to your `Cargo.toml`: 30 - 31 - ```toml 32 - [dependencies] 33 - atproto-identity = "0.2.0" 34 - ``` 35 - 36 - ## Usage 37 - 38 - ### Basic Handle Resolution 39 - 40 - ```rust 41 - use atproto_identity::resolve::{resolve_subject, create_resolver}; 42 - 43 - #[tokio::main] 44 - async fn main() -> anyhow::Result<()> { 45 - let http_client = reqwest::Client::new(); 46 - let dns_resolver = create_resolver(&[]); 47 - 48 - let did = resolve_subject(&http_client, &dns_resolver, "ngerakines.me").await?; 49 - println!("Resolved DID: {}", did); 50 - 51 - Ok(()) 52 - } 53 - ``` 54 - 55 - ### DID Document Retrieval 56 - 57 - ```rust 58 - use atproto_identity::{plc, web}; 59 - 60 - #[tokio::main] 61 - async fn main() -> anyhow::Result<()> { 62 - let http_client = reqwest::Client::new(); 63 - 64 - // Query PLC DID document 65 - let plc_doc = plc::query(&http_client, "plc.directory", "did:plc:example123").await?; 66 - 67 - // Query Web DID document 68 - let web_doc = web::query(&http_client, "did:web:example.com").await?; 69 - 70 - // Convert Web DID to URL (for custom processing) 71 - let did_url = web::did_web_to_url("did:web:example.com")?; 72 - println!("DID document URL: {}", did_url); 73 - 74 - Ok(()) 75 - } 76 - ``` 77 - 78 - ### Web DID URL Conversion 79 - 80 - The `web` module provides utilities for converting DID identifiers to their HTTPS document URLs: 81 - 82 - ```rust 83 - use atproto_identity::web; 84 - 85 - fn main() -> anyhow::Result<()> { 86 - // Convert simple hostname DID 87 - let url = web::did_web_to_url("did:web:example.com")?; 88 - // Returns: "https://example.com/.well-known/did.json" 89 - 90 - // Convert DID with path components 91 - let url = web::did_web_to_url("did:web:example.com:path:subpath")?; 92 - // Returns: "https://example.com/path/subpath/did.json" 93 - 94 - Ok(()) 95 - } 96 - ``` 97 - 98 - ### Cryptographic Key Operations 99 - 100 - The `key` module provides utilities for working with cryptographic keys: 101 - 102 - ```rust 103 - use atproto_identity::key::{identify_key, validate, KeyType}; 104 - 105 - fn main() -> Result<(), Box<dyn std::error::Error>> { 106 - // Identify a key from a DID key string 107 - let key_data = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 108 - 109 - match key_data.0 { 110 - KeyType::K256Public => println!("K-256 public key"), 111 - KeyType::P256Public => println!("P-256 public key"), 112 - KeyType::K256Private => println!("K-256 private key"), 113 - KeyType::P256Private => println!("P-256 private key"), 114 - } 115 - 116 - // Validate a signature (example with dummy data) 117 - let content = b"hello world"; 118 - let signature = vec![0u8; 64]; // Replace with actual signature 119 - validate(&key_data, &signature, content)?; 120 - 121 - Ok(()) 122 - } 123 - ``` 124 - 125 - ### Configuration 126 - 127 - The library supports various configuration options through environment variables: 128 - 129 - ```bash 130 - # Custom PLC directory hostname 131 - export PLC_HOSTNAME=plc.directory 132 - 133 - # Custom DNS nameservers (semicolon-separated) 134 - export DNS_NAMESERVERS=8.8.8.8;1.1.1.1 135 - 136 - # Custom CA certificate bundles (semicolon-separated paths) 137 - export CERTIFICATE_BUNDLES=/path/to/cert1.pem;/path/to/cert2.pem 138 - 139 - # Custom User-Agent string 140 - export USER_AGENT="my-app/1.0" 141 - ``` 142 - 143 - ## Command Line Tool 144 - 145 - The library includes a command-line tool for testing and resolution: 146 - 147 - ```bash 148 - # Install the binary 149 - cargo install --path . 150 - 151 - # Resolve a handle to DID 152 - atproto-identity-resolve ngerakines.me 153 - 154 - # Get full DID document 155 - atproto-identity-resolve --did-document ngerakines.me 156 - ``` 157 - 158 - ## Architecture 159 - 160 - The library is organized into several modules: 161 - 162 - - **resolve**: Core resolution logic for handles and DIDs 163 - - **plc**: PLC directory client for `did:plc` resolution 164 - - **web**: Web DID client for `did:web` resolution and URL conversion 165 - - **model**: Data structures for DID documents and AT Protocol entities 166 - - **validation**: Input validation for handles and DIDs 167 - - **config**: Configuration management and environment variable handling 168 - - **errors**: Structured error types following project conventions 169 - - **key**: Cryptographic key operations including signature validation and key identification for P-256 and K-256 curves 170 - 171 - ## Error Handling 172 - 173 - All errors follow a structured format: 174 - 175 - ``` 176 - error-atproto-identity-<domain>-<number> <message>: <details> 177 - ``` 178 - 179 - Examples: 180 - - `error-atproto-identity-resolve-1 Multiple DIDs resolved for method` 181 - - `error-atproto-identity-plc-1 HTTP request failed: https://plc.directory/did:plc:example Not Found` 182 - - `error-did-web-1 Invalid DID format: missing 'did:web:' prefix` 183 - 184 - ## Contributing 185 - 186 - Contributions are welcome! Please ensure that: 187 - 188 - 1. All tests pass: `cargo test` 189 - 2. Code is properly formatted: `cargo fmt` 190 - 3. No linting issues: `cargo clippy` 191 - 4. New functionality includes appropriate tests 192 - 193 - ## License 194 - 195 - This project is licensed under the MIT License. See the LICENSE file for details. 196 - 197 - ## Acknowledgments 198 - 199 - This library was extracted from the [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application.
+49
crates/atproto-identity/Cargo.toml
··· 1 + [package] 2 + name = "atproto-identity" 3 + version = "0.3.0" 4 + description = "An ATProtocol identity library" 5 + readme = "README.md" 6 + 7 + edition.workspace = true 8 + rust-version.workspace = true 9 + authors.workspace = true 10 + repository.workspace = true 11 + license.workspace = true 12 + keywords.workspace = true 13 + categories.workspace = true 14 + 15 + [[bin]] 16 + name = "atproto-identity-resolve" 17 + test = false 18 + bench = false 19 + doc = true 20 + 21 + [[bin]] 22 + name = "atproto-identity-sign" 23 + test = false 24 + bench = false 25 + doc = true 26 + 27 + [[bin]] 28 + name = "atproto-identity-validate" 29 + test = false 30 + bench = false 31 + doc = true 32 + 33 + [dependencies] 34 + anyhow.workspace = true 35 + ecdsa.workspace = true 36 + hickory-resolver.workspace = true 37 + k256.workspace = true 38 + multibase.workspace = true 39 + p256.workspace = true 40 + reqwest.workspace = true 41 + serde_ipld_dagcbor.workspace = true 42 + serde_json.workspace = true 43 + serde.workspace = true 44 + thiserror.workspace = true 45 + tokio.workspace = true 46 + tracing.workspace = true 47 + 48 + [lints] 49 + workspace = true
+199
crates/atproto-identity/README.md
··· 1 + # atproto-identity 2 + 3 + A Rust library for AT Protocol identity resolution and management. 4 + 5 + ## Overview 6 + 7 + `atproto-identity` provides comprehensive support for resolving and managing identities in the AT Protocol ecosystem. This library handles multiple DID (Decentralized Identifier) methods including `did:plc` and `did:web`, as well as AT Protocol handle resolution via both DNS and HTTP methods. 8 + 9 + This project was extracted from the open-sourced [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project and is designed to be a standalone, reusable library for AT Protocol identity operations. 10 + 11 + ## Features 12 + 13 + - **Handle Resolution**: Resolve AT Protocol handles to DIDs using DNS TXT records and HTTP well-known endpoints 14 + - **DID Document Retrieval**: Fetch and parse DID documents for `did:plc` and `did:web` identifiers 15 + - **Multiple Resolution Methods**: Supports both DNS and HTTP-based handle resolution with conflict detection 16 + - **Configurable DNS**: Custom DNS nameserver support with fallback to system defaults 17 + - **Cryptographic Key Operations**: Support for P-256 and K-256 key identification, signature validation, and signing 18 + - **Structured Logging**: Built-in tracing support for debugging and monitoring 19 + - **Type Safety**: Comprehensive error handling with structured error types 20 + 21 + ## Supported DID Methods 22 + 23 + - **did-method-plc**: Public Ledger of Credentials DIDs via PLC directory 24 + - **did-method-web**: Web-based DIDs following the did:web specification with URL conversion utilities 25 + - **ATProtocol Handle Resolution**: AT Protocol handles (e.g., `ngerakines.me`) can be resolved to DIDs 26 + 27 + ## Installation 28 + 29 + Add this to your `Cargo.toml`: 30 + 31 + ```toml 32 + [dependencies] 33 + atproto-identity = "0.3.0" 34 + ``` 35 + 36 + ## Usage 37 + 38 + ### Basic Handle Resolution 39 + 40 + ```rust 41 + use atproto_identity::resolve::{resolve_subject, create_resolver}; 42 + 43 + #[tokio::main] 44 + async fn main() -> anyhow::Result<()> { 45 + let http_client = reqwest::Client::new(); 46 + let dns_resolver = create_resolver(&[]); 47 + 48 + let did = resolve_subject(&http_client, &dns_resolver, "ngerakines.me").await?; 49 + println!("Resolved DID: {}", did); 50 + 51 + Ok(()) 52 + } 53 + ``` 54 + 55 + ### DID Document Retrieval 56 + 57 + ```rust 58 + use atproto_identity::{plc, web}; 59 + 60 + #[tokio::main] 61 + async fn main() -> anyhow::Result<()> { 62 + let http_client = reqwest::Client::new(); 63 + 64 + // Query PLC DID document 65 + let plc_doc = plc::query(&http_client, "plc.directory", "did:plc:example123").await?; 66 + 67 + // Query Web DID document 68 + let web_doc = web::query(&http_client, "did:web:example.com").await?; 69 + 70 + // Convert Web DID to URL (for custom processing) 71 + let did_url = web::did_web_to_url("did:web:example.com")?; 72 + println!("DID document URL: {}", did_url); 73 + 74 + Ok(()) 75 + } 76 + ``` 77 + 78 + ### Web DID URL Conversion 79 + 80 + The `web` module provides utilities for converting DID identifiers to their HTTPS document URLs: 81 + 82 + ```rust 83 + use atproto_identity::web; 84 + 85 + fn main() -> anyhow::Result<()> { 86 + // Convert simple hostname DID 87 + let url = web::did_web_to_url("did:web:example.com")?; 88 + // Returns: "https://example.com/.well-known/did.json" 89 + 90 + // Convert DID with path components 91 + let url = web::did_web_to_url("did:web:example.com:path:subpath")?; 92 + // Returns: "https://example.com/path/subpath/did.json" 93 + 94 + Ok(()) 95 + } 96 + ``` 97 + 98 + ### Cryptographic Key Operations 99 + 100 + The `key` module provides utilities for working with cryptographic keys: 101 + 102 + ```rust 103 + use atproto_identity::key::{identify_key, validate, KeyType}; 104 + 105 + fn main() -> Result<(), Box<dyn std::error::Error>> { 106 + // Identify a key from a DID key string 107 + let key_data = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 108 + 109 + match key_data.0 { 110 + KeyType::K256Public => println!("K-256 public key"), 111 + KeyType::P256Public => println!("P-256 public key"), 112 + KeyType::K256Private => println!("K-256 private key"), 113 + KeyType::P256Private => println!("P-256 private key"), 114 + } 115 + 116 + // Validate a signature (example with dummy data) 117 + let content = b"hello world"; 118 + let signature = vec![0u8; 64]; // Replace with actual signature 119 + validate(&key_data, &signature, content)?; 120 + 121 + Ok(()) 122 + } 123 + ``` 124 + 125 + ### Configuration 126 + 127 + The library supports various configuration options through environment variables: 128 + 129 + ```bash 130 + # Custom PLC directory hostname 131 + export PLC_HOSTNAME=plc.directory 132 + 133 + # Custom DNS nameservers (semicolon-separated) 134 + export DNS_NAMESERVERS=8.8.8.8;1.1.1.1 135 + 136 + # Custom CA certificate bundles (semicolon-separated paths) 137 + export CERTIFICATE_BUNDLES=/path/to/cert1.pem;/path/to/cert2.pem 138 + 139 + # Custom User-Agent string 140 + export USER_AGENT="my-app/1.0" 141 + ``` 142 + 143 + ## Command Line Tool 144 + 145 + The library includes a command-line tool for testing and resolution: 146 + 147 + ```bash 148 + # Install the binary 149 + cargo install --path . 150 + 151 + # Resolve a handle to DID 152 + atproto-identity-resolve ngerakines.me 153 + 154 + # Get full DID document 155 + atproto-identity-resolve --did-document ngerakines.me 156 + ``` 157 + 158 + ## Architecture 159 + 160 + The library is organized into several modules: 161 + 162 + - **resolve**: Core resolution logic for handles and DIDs 163 + - **plc**: PLC directory client for `did:plc` resolution 164 + - **web**: Web DID client for `did:web` resolution and URL conversion 165 + - **model**: Data structures for DID documents and AT Protocol entities 166 + - **validation**: Input validation for handles and DIDs 167 + - **config**: Configuration management and environment variable handling 168 + - **errors**: Structured error types following project conventions 169 + - **key**: Cryptographic key operations including signature validation and key identification for P-256 and K-256 curves 170 + 171 + ## Error Handling 172 + 173 + All errors follow a structured format: 174 + 175 + ``` 176 + error-atproto-identity-<domain>-<number> <message>: <details> 177 + ``` 178 + 179 + Examples: 180 + - `error-atproto-identity-resolve-1 Multiple DIDs resolved for method` 181 + - `error-atproto-identity-plc-1 HTTP request failed: https://plc.directory/did:plc:example Not Found` 182 + - `error-did-web-1 Invalid DID format: missing 'did:web:' prefix` 183 + 184 + ## Contributing 185 + 186 + Contributions are welcome! Please ensure that: 187 + 188 + 1. All tests pass: `cargo test` 189 + 2. Code is properly formatted: `cargo fmt` 190 + 3. No linting issues: `cargo clippy` 191 + 4. New functionality includes appropriate tests 192 + 193 + ## License 194 + 195 + This project is licensed under the MIT License. See the LICENSE file for details. 196 + 197 + ## Acknowledgments 198 + 199 + This library was extracted from the [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application.
+44
crates/atproto-record/Cargo.toml
··· 1 + [package] 2 + name = "atproto-record" 3 + version = "0.3.0" 4 + description = "AT Protocol record signature operations - cryptographic signing and verification for AT Protocol records" 5 + 6 + edition.workspace = true 7 + rust-version.workspace = true 8 + authors.workspace = true 9 + repository.workspace = true 10 + license.workspace = true 11 + keywords.workspace = true 12 + categories.workspace = true 13 + 14 + [[bin]] 15 + name = "atproto-record-sign" 16 + test = false 17 + bench = false 18 + doc = true 19 + 20 + [[bin]] 21 + name = "atproto-record-verify" 22 + test = false 23 + bench = false 24 + doc = true 25 + 26 + [dependencies] 27 + atproto-identity.workspace = true 28 + 29 + anyhow.workspace = true 30 + ecdsa.workspace = true 31 + k256.workspace = true 32 + multibase.workspace = true 33 + p256.workspace = true 34 + serde_ipld_dagcbor.workspace = true 35 + serde_json.workspace = true 36 + serde.workspace = true 37 + thiserror.workspace = true 38 + tracing.workspace = true 39 + 40 + tokio = {workspace = true} 41 + chrono = {version = "0.4.41", default-features = false, features = ["std", "now"]} 42 + 43 + [lints] 44 + workspace = true
+147
crates/atproto-record/README.md
··· 1 + # atproto-record 2 + 3 + A Rust library for AT Protocol record signature operations, providing cryptographic signing and verification capabilities for AT Protocol records. 4 + 5 + ## Overview 6 + 7 + This crate provides functionality for: 8 + 9 + - **Record Signing**: Create cryptographic signatures for AT Protocol records 10 + - **Signature Verification**: Verify existing signatures against records and public keys 11 + - **Error Handling**: Structured error types for signature operations 12 + - **Multi-curve Support**: Support for P-256 and K-256 elliptic curves via `atproto-identity` 13 + 14 + ## Features 15 + 16 + - Create signatures for AT Protocol records with proper `$sig` object handling 17 + - Required signature object validation (must include `issuer` and `issued_at` fields) 18 + - Verify record signatures against issuer public keys 19 + - IPLD DAG-CBOR serialization for consistent signature generation 20 + - Multibase encoding for signature representation 21 + - Integration with `atproto-identity` for cryptographic key operations 22 + - Repository and collection context support in signature objects 23 + - Comprehensive error handling with structured error types including creation and verification errors 24 + 25 + ## Usage 26 + 27 + ### Creating Signatures 28 + 29 + ```rust 30 + use atproto_record::signature; 31 + use atproto_identity::key::{identify_key, KeyType}; 32 + use serde_json::json; 33 + use atproto_record::errors::VerificationError; 34 + 35 + # async fn example() -> Result<(), VerificationError> { 36 + // Prepare key data 37 + let key_data = identify_key("did:key:example...").map_err(|e| { 38 + VerificationError::KeyOperationFailed(e) 39 + })?; 40 + 41 + // Create a record to sign 42 + let record = json!({ 43 + "$type": "app.bsky.feed.post", 44 + "text": "Hello AT Protocol!", 45 + "createdAt": "2024-01-01T00:00:00Z" 46 + }); 47 + 48 + // Create signature object with required fields 49 + let signature_object = json!({ 50 + "issuer": "did:plc:signer123", 51 + "issued_at": "2024-01-01T00:00:00Z" 52 + }); 53 + 54 + // Create signature 55 + let signed_record = signature::create( 56 + &key_data, 57 + &record, 58 + "did:plc:user123", 59 + "app.bsky.feed.post", 60 + signature_object 61 + ).await?; 62 + # Ok(()) 63 + # } 64 + ``` 65 + 66 + ### Verifying Signatures 67 + 68 + ```rust 69 + use atproto_record::signature; 70 + use atproto_identity::key::identify_key; 71 + use atproto_record::errors::VerificationError; 72 + 73 + # async fn example() -> Result<(), VerificationError> { 74 + // Get the issuer's public key 75 + let issuer_key = identify_key("did:key:issuer...").map_err(|e| { 76 + VerificationError::KeyOperationFailed(e) 77 + })?; 78 + 79 + // Verify the signature 80 + signature::verify( 81 + "did:plc:issuer123", 82 + &issuer_key, 83 + signed_record, 84 + "did:plc:user123", 85 + "app.bsky.feed.post" 86 + ).await?; 87 + # Ok(()) 88 + # } 89 + ``` 90 + 91 + ## Modules 92 + 93 + - [`signature`] - Core signature creation and verification functions 94 + - [`errors`] - Structured error types for signature operations 95 + 96 + ## Error Handling 97 + 98 + The crate uses structured error types defined in the `errors` module: 99 + 100 + ```rust 101 + use atproto_record::errors::VerificationError; 102 + use serde_json::json; 103 + 104 + // Example error handling for signature creation 105 + let signature_object = json!({ "missing": "required_fields" }); 106 + 107 + match signature::create(&key_data, &record, "repo", "collection", signature_object).await { 108 + Ok(signed_record) => println!("Signature created successfully!"), 109 + Err(VerificationError::SignatureObjectMissingField { field }) => { 110 + println!("Missing required field in signature object: {}", field); 111 + } 112 + Err(VerificationError::InvalidSignatureObjectType) => { 113 + println!("Signature object must be a JSON object"); 114 + } 115 + Err(VerificationError::KeyOperationFailed(e)) => { 116 + println!("Cryptographic operation failed: {}", e); 117 + } 118 + Err(e) => println!("Other error: {}", e), 119 + } 120 + 121 + // Example error handling for signature verification 122 + match signature::verify("did:plc:issuer", &key_data, record, "repo", "collection").await { 123 + Ok(()) => println!("Signature valid!"), 124 + Err(VerificationError::NoValidSignatureForIssuer { issuer }) => { 125 + println!("No valid signature found for issuer: {}", issuer); 126 + } 127 + Err(VerificationError::NoSignaturesField) => { 128 + println!("Record contains no signatures field"); 129 + } 130 + Err(e) => println!("Verification failed: {}", e), 131 + } 132 + ``` 133 + 134 + ## Dependencies 135 + 136 + This crate builds on: 137 + 138 + - [`atproto-identity`](../atproto-identity) - Cryptographic key operations and DID resolution 139 + - `serde_ipld_dagcbor` - IPLD DAG-CBOR serialization for signature content 140 + - `multibase` - Base encoding for signature representation 141 + - `serde_json` - JSON handling for AT Protocol records 142 + - `anyhow` - Error handling utilities 143 + - `thiserror` - Structured error type derivation 144 + 145 + ## License 146 + 147 + Licensed under the MIT License.
+226
crates/atproto-record/src/bin/atproto-record-sign.rs
··· 1 + use anyhow::{anyhow, Result}; 2 + use atproto_identity::{ 3 + key::{identify_key, KeyType}, 4 + resolve::{parse_input, InputType}, 5 + }; 6 + use atproto_record::signature::create; 7 + use chrono::{SecondsFormat, Utc}; 8 + use serde_json::json; 9 + use std::{collections::HashMap, env, fs}; 10 + 11 + /// AT Protocol Record Signing Tool 12 + /// 13 + /// 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, 15 + /// and outputs the signed record with embedded signature metadata. 16 + /// 17 + /// ## Overview 18 + /// 19 + /// The tool performs the following operations: 20 + /// 1. **Command Line Parsing**: Extracts signing parameters from command line arguments 21 + /// 2. **Key Resolution**: Converts DID key strings to cryptographic key material 22 + /// 3. **Record Loading**: Reads and parses JSON records from disk files 23 + /// 4. **Signature Creation**: Generates cryptographic signatures using IPLD DAG-CBOR serialization 24 + /// 5. **Output Generation**: Produces signed records with embedded signature objects 25 + /// 26 + /// ## Signature Process 27 + /// 28 + /// The signing process follows AT Protocol conventions: 29 + /// - Creates a `$sig` object with repository and collection context 30 + /// - Serializes the record with `$sig` using IPLD DAG-CBOR format 31 + /// - Generates ECDSA signatures using P-256 or K-256 curves 32 + /// - Embeds signatures in a `signatures` array with issuer metadata 33 + /// - Encodes signatures using multibase (base64url) 34 + /// 35 + /// ## Arguments 36 + /// 37 + /// The tool accepts flexible argument ordering: 38 + /// - **DID Key** (`did:key:...`) - Cryptographic key for signing operations 39 + /// - **Issuer DID** (`did:plc:...` or `did:web:...`) - Identity of the signature issuer 40 + /// - **Record File** (file path) - JSON file containing the record to sign 41 + /// - **Parameters** (`key=value`) - Repository, collection, and signature metadata 42 + /// 43 + /// ## Required Parameters 44 + /// 45 + /// - `repository=<DID>` - Repository context for the signature 46 + /// - `collection=<name>` - Collection type context for the signature 47 + /// 48 + /// ## Optional Parameters 49 + /// 50 + /// - `issued_at=<timestamp>` - RFC 3339 timestamp (defaults to current time) 51 + /// - Custom fields can be added to the signature object via `key=value` pairs 52 + /// 53 + /// ## Examples 54 + /// 55 + /// ### Basic Usage 56 + /// ```bash 57 + /// atproto-record-sign \ 58 + /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 59 + /// ./post.json \ 60 + /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 61 + /// repository=did:plc:4zutorghlchjxzgceklue4la \ 62 + /// collection=app.bsky.feed.post 63 + /// ``` 64 + /// 65 + /// ### With Custom Timestamp 66 + /// ```bash 67 + /// atproto-record-sign \ 68 + /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 69 + /// ./badge.json \ 70 + /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 71 + /// repository=did:plc:4zutorghlchjxzgceklue4la \ 72 + /// collection=community.lexicon.badge.award \ 73 + /// issued_at=2025-05-16T14:00:02.000Z 74 + /// ``` 75 + /// 76 + /// ### Input Record Example (`post.json`) 77 + /// ```json 78 + /// { 79 + /// "$type": "app.bsky.feed.post", 80 + /// "text": "Hello AT Protocol!", 81 + /// "createdAt": "2024-01-01T00:00:00Z" 82 + /// } 83 + /// ``` 84 + /// 85 + /// ### Output Signed Record 86 + /// ```json 87 + /// { 88 + /// "$type": "app.bsky.feed.post", 89 + /// "text": "Hello AT Protocol!", 90 + /// "createdAt": "2024-01-01T00:00:00Z", 91 + /// "signatures": [ 92 + /// { 93 + /// "issuer": "did:plc:tgudj2fjm77pzkuawquqhsxm", 94 + /// "issued_at": "2025-05-30T16:27:20.532Z", 95 + /// "signature": "uo36qFvSGV6QcFaxYYN9JCAGQNv2yVHK2LPN3lNp210v..." 96 + /// } 97 + /// ] 98 + /// } 99 + /// ``` 100 + /// 101 + /// ## Error Handling 102 + /// 103 + /// The tool provides detailed error messages for: 104 + /// - Missing or invalid DID keys and issuer DIDs 105 + /// - File reading and JSON parsing failures 106 + /// - Missing required parameters (repository, collection) 107 + /// - Cryptographic operation failures 108 + /// - Unsupported DID methods 109 + /// 110 + /// ## Security Considerations 111 + /// 112 + /// - Private keys are handled in-memory only and not persisted 113 + /// - Signatures are generated using industry-standard ECDSA algorithms 114 + /// - All cryptographic operations follow AT Protocol specifications 115 + /// - Input validation prevents malformed DID and parameter injection 116 + #[tokio::main] 117 + async fn main() -> Result<()> { 118 + // Check for help flags 119 + let args: Vec<String> = env::args().skip(1).collect(); 120 + if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") { 121 + println!("AT Protocol Record Signing Tool"); 122 + println!(); 123 + println!("USAGE:"); 124 + println!(" atproto-record-sign <ISSUER_DID> <SIGNING_KEY> <RECORD_FILE> repository=<REPO> collection=<COLLECTION> [key=value...]"); 125 + println!(); 126 + 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"); 130 + println!(); 131 + println!("REQUIRED PARAMETERS:"); 132 + println!(" repository=<REPO> Repository DID context"); 133 + println!(" collection=<COLLECTION> Collection name context"); 134 + println!(); 135 + println!("OPTIONAL PARAMETERS:"); 136 + println!(" issued_at=<TIMESTAMP> RFC 3339 timestamp (defaults to current time)"); 137 + println!(" [key=value...] Additional fields for signature object"); 138 + println!(); 139 + println!("EXAMPLE:"); 140 + println!(" atproto-record-sign \\"); 141 + println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\"); 142 + println!(" ./record.json \\"); 143 + println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\"); 144 + println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\"); 145 + println!(" collection=community.lexicon.badge.award"); 146 + return Ok(()); 147 + } 148 + 149 + let arguments = args.into_iter(); 150 + 151 + let mut collection: Option<String> = None; 152 + let mut repository: Option<String> = None; 153 + let mut record: Option<serde_json::Value> = None; 154 + let mut issuer: Option<String> = None; 155 + let mut key_data: Option<(KeyType, Vec<u8>)> = None; 156 + let mut signature_extras: HashMap<String, String> = HashMap::default(); 157 + 158 + for argument in arguments { 159 + if let Some((key, value)) = argument.split_once("=") { 160 + match key { 161 + "collection" => { 162 + collection = Some(value.to_string()); 163 + } 164 + "repository" => { 165 + repository = Some(value.to_string()); 166 + } 167 + _ => { 168 + signature_extras.insert(key.to_string(), value.to_string()); 169 + } 170 + } 171 + } else if argument.starts_with("did:key:") { 172 + // Parse the did:key to extract key data for signing 173 + key_data = Some(identify_key(&argument)?); 174 + } else if argument.starts_with("did:") { 175 + match parse_input(&argument) { 176 + Ok(InputType::Plc(did)) | Ok(InputType::Web(did)) => { 177 + issuer = Some(did); 178 + } 179 + Ok(_) => return Err(anyhow!("Unsupported DID method: {}", argument)), 180 + Err(e) => return Err(anyhow!("Failed to parse DID {}: {}", argument, e)), 181 + } 182 + } else { 183 + // Assume it's a file path to read the record from 184 + if record.is_none() { 185 + let file_content = fs::read_to_string(&argument) 186 + .map_err(|e| anyhow!("Failed to read file {}: {}", argument, e))?; 187 + record = 188 + Some(serde_json::from_str(&file_content).map_err(|e| { 189 + anyhow!("Failed to parse JSON from file {}: {}", argument, e) 190 + })?); 191 + } else { 192 + return Err(anyhow!("Unexpected argument: {}", argument)); 193 + } 194 + } 195 + } 196 + 197 + let collection = collection.ok_or(anyhow!("missing collection value"))?; 198 + let repository = repository.ok_or(anyhow!("missing repository value"))?; 199 + let record = record.ok_or(anyhow!("missing record source value"))?; 200 + let issuer = issuer.ok_or(anyhow!("missing issuer value"))?; 201 + let key_data = key_data.ok_or(anyhow!("missing key data"))?; 202 + 203 + // Write "issuer" key to signature_extras 204 + signature_extras.insert("issuer".to_string(), issuer); 205 + 206 + // If signature_extras does not contain "issued_at" key, create a RFC 3339 formatted timestamp 207 + if !signature_extras.contains_key("issued_at") { 208 + let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); 209 + signature_extras.insert("issued_at".to_string(), timestamp); 210 + } 211 + 212 + let signature_object = json!(signature_extras); 213 + let signed_record = create( 214 + &key_data, 215 + &record, 216 + &repository, 217 + &collection, 218 + signature_object, 219 + ) 220 + .await?; 221 + 222 + let pretty_signed_record = serde_json::to_string_pretty(&signed_record); 223 + println!("{}", pretty_signed_record.unwrap()); 224 + 225 + Ok(()) 226 + }
+218
crates/atproto-record/src/bin/atproto-record-verify.rs
··· 1 + use anyhow::{anyhow, Result}; 2 + use atproto_identity::{ 3 + key::{identify_key, KeyType}, 4 + resolve::{parse_input, InputType}, 5 + }; 6 + use atproto_record::signature::verify; 7 + use std::{env, fs}; 8 + 9 + /// AT Protocol Record Verification Tool 10 + /// 11 + /// 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, 13 + /// and reports whether the signature verification succeeds or fails. 14 + /// 15 + /// ## Overview 16 + /// 17 + /// The tool performs the following operations: 18 + /// 1. **Command Line Parsing**: Extracts verification parameters from command line arguments 19 + /// 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 21 + /// 4. **Signature Verification**: Validates cryptographic signatures using IPLD DAG-CBOR deserialization 22 + /// 5. **Result Reporting**: Outputs verification success or detailed failure information 23 + /// 24 + /// ## Verification Process 25 + /// 26 + /// The verification process follows AT Protocol conventions: 27 + /// - Extracts signatures from the `signatures` array in the record 28 + /// - Finds signatures matching the specified issuer DID 29 + /// - Reconstructs the `$sig` object with repository and collection context 30 + /// - Deserializes the record with `$sig` using IPLD DAG-CBOR format 31 + /// - Validates ECDSA signatures using P-256 or K-256 curves 32 + /// - Decodes signatures from multibase (base64url) encoding 33 + /// - Verifies cryptographic signatures against the public key 34 + /// 35 + /// ## Arguments 36 + /// 37 + /// The tool accepts flexible argument ordering: 38 + /// - **Issuer DID** (`did:plc:...` or `did:web:...`) - Identity of the expected signature issuer 39 + /// - **Verification Key** (`did:key:...`) - Public key for signature verification 40 + /// - **Record File** (file path) - JSON file containing the signed record to verify 41 + /// - **Parameters** (`key=value`) - Repository and collection context for verification 42 + /// 43 + /// ## Required Parameters 44 + /// 45 + /// - `repository=<DID>` - Repository context that was used during signing 46 + /// - `collection=<name>` - Collection type context that was used during signing 47 + /// 48 + /// ## Examples 49 + /// 50 + /// ### Basic Verification 51 + /// ```bash 52 + /// atproto-record-verify \ 53 + /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 54 + /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 55 + /// ./signed_post.json \ 56 + /// repository=did:plc:4zutorghlchjxzgceklue4la \ 57 + /// collection=app.bsky.feed.post 58 + /// ``` 59 + /// 60 + /// ### Verifying Badge Awards 61 + /// ```bash 62 + /// atproto-record-verify \ 63 + /// did:plc:tgudj2fjm77pzkuawquqhsxm \ 64 + /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 65 + /// ./signed_badge.json \ 66 + /// repository=did:plc:4zutorghlchjxzgceklue4la \ 67 + /// collection=community.lexicon.badge.award 68 + /// ``` 69 + /// 70 + /// ### Input Signed Record Example (`signed_post.json`) 71 + /// ```json 72 + /// { 73 + /// "$type": "app.bsky.feed.post", 74 + /// "text": "Hello AT Protocol!", 75 + /// "createdAt": "2024-01-01T00:00:00Z", 76 + /// "signatures": [ 77 + /// { 78 + /// "issuer": "did:plc:tgudj2fjm77pzkuawquqhsxm", 79 + /// "issued_at": "2025-05-30T16:27:20.532Z", 80 + /// "signature": "uo36qFvSGV6QcFaxYYN9JCAGQNv2yVHK2LPN3lNp210v..." 81 + /// } 82 + /// ] 83 + /// } 84 + /// ``` 85 + /// 86 + /// ### Successful Verification Output 87 + /// ``` 88 + /// OK 89 + /// ``` 90 + /// 91 + /// ### Failed Verification Output 92 + /// ``` 93 + /// Error: Signature verification failed: error validating signature: ... 94 + /// ``` 95 + /// 96 + /// ## Verification Workflow 97 + /// 98 + /// 1. **Parse Arguments**: Extract issuer DID, verification key, record file, and context parameters 99 + /// 2. **Load Record**: Read and parse the signed JSON record from the specified file 100 + /// 3. **Extract Signatures**: Find signature objects in the `signatures` array matching the issuer 101 + /// 4. **Reconstruct Context**: Recreate the `$sig` object with repository and collection context 102 + /// 5. **Serialize Content**: Convert the record with `$sig` to IPLD DAG-CBOR format 103 + /// 6. **Decode Signature**: Convert multibase-encoded signature to raw bytes 104 + /// 7. **Verify Cryptographically**: Validate the signature against the serialized content using the public key 105 + /// 8. **Report Result**: Output "OK" for valid signatures or detailed error messages for failures 106 + /// 107 + /// ## Error Handling 108 + /// 109 + /// The tool provides detailed error messages for: 110 + /// - Missing or invalid DID keys and issuer DIDs 111 + /// - File reading and JSON parsing failures 112 + /// - Missing required parameters (repository, collection) 113 + /// - Records without signature fields or matching issuer signatures 114 + /// - Signature decoding and deserialization failures 115 + /// - Cryptographic verification failures 116 + /// - Unsupported DID methods and malformed signatures 117 + /// 118 + /// ## Security Considerations 119 + /// 120 + /// - Public keys are used for verification only, no private key handling 121 + /// - Signature verification uses industry-standard ECDSA algorithms 122 + /// - All cryptographic operations follow AT Protocol specifications 123 + /// - Input validation prevents malformed DID and parameter injection 124 + /// - Failed verifications provide diagnostic information without exposing sensitive data 125 + /// 126 + /// ## Integration with Signing Tool 127 + /// 128 + /// This tool is designed to work with records produced by `atproto-record-sign`: 129 + /// 1. Use `atproto-record-sign` to create signed records with embedded signatures 130 + /// 2. Use `atproto-record-verify` to validate those signatures using the corresponding public key 131 + /// 3. The same repository and collection parameters must be used for both signing and verification 132 + /// 4. The verification key should correspond to the private key used for signing 133 + #[tokio::main] 134 + 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 Verifying Tool"); 139 + println!(); 140 + println!("USAGE:"); 141 + println!(" atproto-record-verify <ISSUER_DID> <KEY> <RECORD_FILE> repository=<REPO> collection=<COLLECTION> [key=value...]"); 142 + println!(); 143 + 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"); 147 + println!(); 148 + println!("REQUIRED PARAMETERS:"); 149 + println!(" repository=<REPO> Repository DID context"); 150 + println!(" collection=<COLLECTION> Collection name context"); 151 + println!(); 152 + println!("EXAMPLE:"); 153 + println!(" atproto-record-verify \\"); 154 + println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\"); 155 + println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\"); 156 + println!(" ./record.json \\"); 157 + println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\"); 158 + println!(" collection=community.lexicon.badge.award"); 159 + return Ok(()); 160 + } 161 + 162 + let arguments = args.into_iter(); 163 + 164 + let mut collection: Option<String> = None; 165 + let mut repository: Option<String> = None; 166 + let mut record: Option<serde_json::Value> = None; 167 + let mut issuer: Option<String> = None; 168 + let mut key_data: Option<(KeyType, Vec<u8>)> = None; 169 + 170 + for argument in arguments { 171 + if let Some((key, value)) = argument.split_once("=") { 172 + match key { 173 + "collection" => { 174 + collection = Some(value.to_string()); 175 + } 176 + "repository" => { 177 + repository = Some(value.to_string()); 178 + } 179 + _ => {} 180 + } 181 + } else if argument.starts_with("did:key:") { 182 + // Parse the did:key to extract key data for verification 183 + key_data = Some(identify_key(&argument)?); 184 + } else if argument.starts_with("did:") { 185 + match parse_input(&argument) { 186 + Ok(InputType::Plc(did)) | Ok(InputType::Web(did)) => { 187 + issuer = Some(did); 188 + } 189 + Ok(_) => return Err(anyhow!("Unsupported DID method: {}", argument)), 190 + Err(e) => return Err(anyhow!("Failed to parse DID {}: {}", argument, e)), 191 + } 192 + } else { 193 + // Assume it's a file path to read the record from 194 + if record.is_none() { 195 + let file_content = fs::read_to_string(&argument) 196 + .map_err(|e| anyhow!("Failed to read file {}: {}", argument, e))?; 197 + record = 198 + Some(serde_json::from_str(&file_content).map_err(|e| { 199 + anyhow!("Failed to parse JSON from file {}: {}", argument, e) 200 + })?); 201 + } else { 202 + return Err(anyhow!("Unexpected argument: {}", argument)); 203 + } 204 + } 205 + } 206 + 207 + let collection = collection.ok_or(anyhow!("missing collection value"))?; 208 + let repository = repository.ok_or(anyhow!("missing repository value"))?; 209 + let record = record.ok_or(anyhow!("missing record source value"))?; 210 + let issuer = issuer.ok_or(anyhow!("missing issuer value"))?; 211 + let key_data = key_data.ok_or(anyhow!("missing key data"))?; 212 + 213 + verify(&issuer, &key_data, record, &repository, &collection).await?; 214 + 215 + println!("OK"); 216 + 217 + Ok(()) 218 + }
+110
crates/atproto-record/src/errors.rs
··· 1 + use thiserror::Error; 2 + 3 + /// Represents errors that can occur during record signature verification operations. 4 + /// 5 + /// These errors relate to various stages of the signature verification process 6 + /// for AT Protocol records, from parsing to cryptographic validation. 7 + #[derive(Debug, Error)] 8 + pub enum VerificationError { 9 + /// Error when AT-URI parsing fails. 10 + /// 11 + /// This error occurs when the provided AT-URI is malformed or cannot 12 + /// be parsed into its component parts (repository, collection, rkey). 13 + #[error("error-atproto-record-verification-1 AT-URI parsing failed: {0}")] 14 + AturiParsingFailed(#[from] anyhow::Error), 15 + 16 + /// Error when no signatures field is found in the record. 17 + /// 18 + /// This error occurs when a record does not contain either a "signatures" 19 + /// or "sigs" field, which is required for signature verification. 20 + #[error("error-atproto-record-verification-2 No signatures field found in record")] 21 + NoSignaturesField, 22 + 23 + /// Error when issuer field is missing from a signature object. 24 + /// 25 + /// This error occurs when a signature object in the signatures array 26 + /// does not contain the required "issuer" field. 27 + #[error("error-atproto-record-verification-3 Missing issuer field in signature object")] 28 + MissingIssuerField, 29 + 30 + /// Error when signature field is missing from a signature object. 31 + /// 32 + /// This error occurs when a signature object in the signatures array 33 + /// does not contain the required "signature" field. 34 + #[error("error-atproto-record-verification-4 Missing signature field in signature object")] 35 + MissingSignatureField, 36 + 37 + /// Error when public key parsing fails. 38 + /// 39 + /// This error occurs when the decoded public key bytes cannot be 40 + /// parsed into a valid P-256 public key structure. 41 + #[error("error-atproto-record-verification-5 Public key parsing failed: {0}")] 42 + PublicKeyParsingFailed(p256::elliptic_curve::Error), 43 + 44 + /// Error when record serialization fails. 45 + /// 46 + /// This error occurs when the signed record cannot be serialized 47 + /// to IPLD CBOR format for signature verification. 48 + #[error("error-atproto-record-verification-6 Record serialization failed: {0}")] 49 + RecordSerializationFailed( 50 + #[from] serde_ipld_dagcbor::EncodeError<std::collections::TryReserveError>, 51 + ), 52 + 53 + /// Error when signature parsing fails. 54 + /// 55 + /// This error occurs when the decoded signature bytes cannot be 56 + /// parsed into a valid P-256 ECDSA signature structure. 57 + #[error("error-atproto-record-verification-7 Signature parsing failed: {0}")] 58 + SignatureParsingFailed(p256::ecdsa::Error), 59 + 60 + /// Error when signature verification fails. 61 + /// 62 + /// This error occurs when the cryptographic verification of the 63 + /// signature against the signed record fails, indicating the 64 + /// signature is invalid for the given data and public key. 65 + #[error("error-atproto-record-verification-8 Signature verification failed: {0}")] 66 + SignatureVerificationFailed(anyhow::Error), 67 + 68 + /// Error when no valid signature is found for the specified issuer. 69 + /// 70 + /// This error occurs when none of the signatures in the record 71 + /// belong to the specified issuer, or when all signatures from 72 + /// the issuer fail verification. 73 + #[error("error-atproto-record-verification-9 No valid signature found for issuer: {issuer}")] 74 + NoValidSignatureForIssuer { 75 + /// The issuer DID for which no valid signature was found 76 + issuer: String, 77 + }, 78 + 79 + /// Error when no valid signature is found in the record. 80 + /// 81 + /// This error occurs when a record contains no valid signatures 82 + /// that can be verified, typically used as a fallback error. 83 + #[error("error-atproto-record-verification-10 No valid signature")] 84 + NoValidSignature, 85 + 86 + /// Error when signature object is not a valid JSON object. 87 + /// 88 + /// This error occurs when the provided signature object parameter 89 + /// is not a JSON object type, which is required for signature creation. 90 + #[error("error-atproto-record-verification-11 Signature object must be a JSON object")] 91 + InvalidSignatureObjectType, 92 + 93 + /// Error when signature object is missing fields. 94 + /// 95 + /// This error occurs when the signature object is missing fields that are 96 + /// necessary use during signature creation, such as 'issuer' or 97 + /// 'issued_at'. 98 + #[error("error-atproto-record-verification-12 Signature object missing field: {field}")] 99 + SignatureObjectMissingField { 100 + /// The name of the missing field 101 + field: String, 102 + }, 103 + 104 + /// Error when cryptographic key operations fail. 105 + /// 106 + /// This error occurs when key-related operations such as signing 107 + /// or key parsing fail during signature creation or verification. 108 + #[error("error-atproto-record-verification-13 Key operation failed: {0}")] 109 + KeyOperationFailed(#[from] atproto_identity::errors::KeyError), 110 + }
+130
crates/atproto-record/src/lib.rs
··· 1 + //! # atproto-record 2 + //! 3 + //! A Rust library for AT Protocol record signature operations, providing cryptographic 4 + //! signing and verification capabilities for AT Protocol records. 5 + //! 6 + //! This crate handles the cryptographic aspects of AT Protocol record attestation, 7 + //! including signature creation and verification with proper `$sig` object handling 8 + //! and IPLD DAG-CBOR serialization. 9 + //! 10 + //! ## Core Modules 11 + //! 12 + //! - [`signature`] - Core signature creation and verification functions 13 + //! - [`errors`] - Structured error types for signature verification operations 14 + //! 15 + //! ## Key Features 16 + //! 17 + //! - **Record Signing**: Create cryptographic signatures for AT Protocol records 18 + //! - **Signature Verification**: Verify existing signatures against records and public keys 19 + //! - **Multi-curve Support**: Support for P-256 and K-256 elliptic curves via `atproto-identity` 20 + //! - **IPLD Integration**: Proper IPLD DAG-CBOR serialization for signature content 21 + //! - **Error Handling**: Comprehensive structured error types following project conventions 22 + //! 23 + //! ## Usage Examples 24 + //! 25 + //! ### Creating a Signature 26 + //! 27 + //! ```rust 28 + //! use atproto_record::signature; 29 + //! use atproto_record::errors::VerificationError; 30 + //! use serde_json::json; 31 + //! use atproto_identity::key::{KeyType}; 32 + //! async fn example() -> Result<(), VerificationError> { 33 + //! let key_data = (KeyType::P256Public, vec![]); 34 + //! 35 + //! let record = json!({ 36 + //! "$type": "app.bsky.feed.post", 37 + //! "text": "Hello AT Protocol!" 38 + //! }); 39 + //! 40 + //! // Create signature object with required fields 41 + //! let signature_object = json!({ 42 + //! "issuer": "did:plc:signer123", 43 + //! "issued_at": "2024-01-01T00:00:00Z" 44 + //! }); 45 + //! 46 + //! let signed_record = signature::create( 47 + //! &key_data, 48 + //! &record, 49 + //! "did:plc:user123", 50 + //! "app.bsky.feed.post", 51 + //! signature_object 52 + //! ).await?; 53 + //! Ok(()) 54 + //! } 55 + //! ``` 56 + //! 57 + //! ### Verifying a Signature 58 + //! 59 + //! ```rust 60 + //! use atproto_record::signature; 61 + //! use atproto_record::errors::VerificationError; 62 + //! use atproto_identity::key::{KeyType}; 63 + //! use serde_json::json; 64 + //! async fn example() -> Result<(), VerificationError> { 65 + //! let issuer_key = (KeyType::P256Public, vec![]); 66 + //! let signed_record = json!({ 67 + //! "signatures": [{ 68 + //! "issuer": "did:plc:issuer123", 69 + //! "signature": "uExample_signature_data" 70 + //! }] 71 + //! }); 72 + //! 73 + //! signature::verify( 74 + //! "did:plc:issuer123", 75 + //! &issuer_key, 76 + //! signed_record, 77 + //! "did:plc:user123", 78 + //! "app.bsky.feed.post" 79 + //! ).await?; 80 + //! Ok(()) 81 + //! } 82 + //! ``` 83 + //! 84 + //! ## Error Handling 85 + //! 86 + //! All signature operations return structured errors defined in the [`errors`] module: 87 + //! 88 + //! ```rust 89 + //! use atproto_record::errors::VerificationError; 90 + //! use atproto_record::signature::create; 91 + //! use serde_json::json; 92 + //! 93 + //! // Example: handling signature creation errors 94 + //! async fn example() -> Result<(), Box<dyn std::error::Error>> { 95 + //! let key_data = (atproto_identity::key::KeyType::P256Public, vec![]); 96 + //! let record = json!({}); 97 + //! let invalid_signature_object = json!({ "missing": "required_fields" }); 98 + //! 99 + //! match create(&key_data, &record, "repo", "collection", invalid_signature_object).await { 100 + //! Ok(_) => println!("Signature created successfully!"), 101 + //! Err(VerificationError::SignatureObjectMissingField { field }) => { 102 + //! println!("Missing required field: {}", field); 103 + //! } 104 + //! Err(VerificationError::InvalidSignatureObjectType) => { 105 + //! println!("Signature object must be a JSON object"); 106 + //! } 107 + //! Err(VerificationError::KeyOperationFailed(_)) => { 108 + //! println!("Cryptographic operation failed"); 109 + //! } 110 + //! Err(e) => println!("Other error: {}", e), 111 + //! } 112 + //! Ok(()) 113 + //! } 114 + //! ``` 115 + 116 + #![warn(missing_docs)] 117 + 118 + /// Structured error types for signature verification operations. 119 + /// 120 + /// This module defines comprehensive error types that can occur during 121 + /// AT Protocol record signature verification, following the project's 122 + /// error naming conventions. 123 + pub mod errors; 124 + 125 + /// Core signature creation and verification functions. 126 + /// 127 + /// This module provides the primary functionality for creating and verifying 128 + /// cryptographic signatures on AT Protocol records, with proper handling of 129 + /// signature objects and IPLD serialization. 130 + pub mod signature;
+195
crates/atproto-record/src/signature.rs
··· 1 + use anyhow::anyhow; 2 + use atproto_identity::key::{sign, validate, KeyType}; 3 + use serde_json::json; 4 + 5 + use crate::errors::VerificationError; 6 + 7 + /// Signs an AT Protocol record. 8 + /// 9 + /// This function generates a signature for the provided record using the specified 10 + /// key data and context information. The signature is embedded into the record 11 + /// following AT Protocol signature conventions. 12 + /// 13 + /// # Parameters 14 + /// 15 + /// * `key_data` - The cryptographic key information (key type and bytes) 16 + /// * `record` - The JSON record to be signed 17 + /// * `repository` - The repository DID context for the signature 18 + /// * `collection` - The collection name context for the signature 19 + /// * `signature_object` - Optional additional fields for the signature object 20 + /// 21 + /// # Returns 22 + /// 23 + /// Returns the original record with a `signatures` field containing the new signature. 24 + /// 25 + /// # Errors 26 + /// 27 + /// Returns an error if: 28 + /// - IPLD serialization fails 29 + /// - Cryptographic signing fails 30 + /// - JSON manipulation fails 31 + pub async fn create( 32 + key_data: &(KeyType, Vec<u8>), 33 + record: &serde_json::Value, 34 + repository: &str, 35 + collection: &str, 36 + signature_object: serde_json::Value, 37 + ) -> Result<serde_json::Value, VerificationError> { 38 + if let Some(record_map) = signature_object.as_object() { 39 + if !record_map.contains_key("issuer") { 40 + return Err(VerificationError::SignatureObjectMissingField { 41 + field: "issuer".to_string(), 42 + }); 43 + } 44 + if !record_map.contains_key("issued_at") { 45 + return Err(VerificationError::SignatureObjectMissingField { 46 + field: "issued_at".to_string(), 47 + }); 48 + } 49 + } else { 50 + return Err(VerificationError::InvalidSignatureObjectType); 51 + }; 52 + 53 + // Prepare the $sig object. 54 + let mut sig = signature_object.clone(); 55 + if let Some(record_map) = sig.as_object_mut() { 56 + record_map.insert("repository".to_string(), json!(repository)); 57 + record_map.insert("collection".to_string(), json!(collection)); 58 + } 59 + 60 + // Create a copy of the record with the $sig object for signing. 61 + let mut signing_record = record.clone(); 62 + if let Some(record_map) = signing_record.as_object_mut() { 63 + record_map.remove("signatures"); 64 + record_map.remove("$sig"); 65 + record_map.insert("$sig".to_string(), sig); 66 + } 67 + 68 + // Create a signature. 69 + let serialized_signing_record = serde_ipld_dagcbor::to_vec(&signing_record)?; 70 + 71 + let signature = sign(key_data, &serialized_signing_record)?; 72 + let encoded_signature = multibase::encode(multibase::Base::Base64Url, &signature); 73 + 74 + // Compose the proof object 75 + let mut proof = signature_object.clone(); 76 + if let Some(record_map) = proof.as_object_mut() { 77 + record_map.remove("repository"); 78 + record_map.remove("collection"); 79 + record_map.insert("signature".to_string(), json!(encoded_signature)); 80 + } 81 + 82 + // Add the signature to the original record 83 + let mut signed_record = record.clone(); 84 + 85 + if let Some(record_map) = signed_record.as_object_mut() { 86 + let mut signatures: Vec<serde_json::Value> = record 87 + .get("signatures") 88 + .and_then(|v| v.as_array().cloned()) 89 + .unwrap_or_default(); 90 + 91 + signatures.push(proof); 92 + 93 + record_map.remove("$sig"); 94 + record_map.remove("signatures"); 95 + 96 + // Add the $sig field 97 + record_map.insert("signatures".to_string(), json!(signatures)); 98 + } 99 + 100 + Ok(signed_record) 101 + } 102 + 103 + /// Verifies a cryptographic signature on an AT Protocol record. 104 + /// 105 + /// This function validates that the provided record contains a valid signature 106 + /// from the specified issuer using the provided public key. It reconstructs 107 + /// the signed content and verifies the cryptographic signature. 108 + /// 109 + /// # Parameters 110 + /// 111 + /// * `issuer` - The DID of the expected signature issuer 112 + /// * `key_data` - The public key information for verification 113 + /// * `record` - The signed JSON record to verify 114 + /// * `repository` - The repository DID context for verification 115 + /// * `collection` - The collection name context for verification 116 + /// 117 + /// # Returns 118 + /// 119 + /// Returns `Ok(())` if a valid signature from the issuer is found and verified. 120 + /// 121 + /// # Errors 122 + /// 123 + /// Returns a [`VerificationError`] if: 124 + /// - No signatures field is found in the record 125 + /// - No signature from the specified issuer is found 126 + /// - Signature decoding or parsing fails 127 + /// - Cryptographic verification fails 128 + /// - Record serialization fails 129 + pub async fn verify( 130 + issuer: &str, 131 + key_data: &(KeyType, Vec<u8>), 132 + record: serde_json::Value, 133 + repository: &str, 134 + collection: &str, 135 + ) -> Result<(), VerificationError> { 136 + let signatures = record 137 + .get("sigs") 138 + .or_else(|| record.get("signatures")) 139 + .and_then(|v| v.as_array()) 140 + .ok_or(VerificationError::NoSignaturesField)?; 141 + 142 + for sig_obj in signatures { 143 + // Extract the issuer from the signature object 144 + let signature_issuer = sig_obj 145 + .get("issuer") 146 + .and_then(|v| v.as_str()) 147 + .ok_or(VerificationError::MissingIssuerField)?; 148 + 149 + let signature_value = sig_obj 150 + .get("signature") 151 + .and_then(|v| v.as_str()) 152 + .ok_or(VerificationError::MissingSignatureField)?; 153 + 154 + if issuer != signature_issuer { 155 + continue; 156 + } 157 + 158 + let mut sig_variable = sig_obj.clone(); 159 + 160 + if let Some(sig_map) = sig_variable.as_object_mut() { 161 + sig_map.remove("signature"); 162 + sig_map.insert("repository".to_string(), json!(repository)); 163 + sig_map.insert("collection".to_string(), json!(collection)); 164 + } 165 + 166 + let mut signed_record = record.clone(); 167 + if let Some(record_map) = signed_record.as_object_mut() { 168 + record_map.remove("signatures"); 169 + record_map.insert("$sig".to_string(), sig_variable); 170 + } 171 + 172 + let serialized_record = serde_ipld_dagcbor::to_vec(&signed_record) 173 + .map_err(VerificationError::RecordSerializationFailed)?; 174 + 175 + let (_, signature_bytes) = multibase::decode(signature_value).map_err(|e| { 176 + VerificationError::SignatureVerificationFailed(anyhow!( 177 + "error decoding signature: {}", 178 + e 179 + )) 180 + })?; 181 + 182 + validate(key_data, &signature_bytes, &serialized_record).map_err(|e| { 183 + VerificationError::SignatureVerificationFailed(anyhow!( 184 + "error validating signature: {}", 185 + e 186 + )) 187 + })?; 188 + 189 + return Ok(()); 190 + } 191 + 192 + Err(VerificationError::NoValidSignatureForIssuer { 193 + issuer: issuer.to_string(), 194 + }) 195 + }
src/bin/atproto-identity-resolve.rs crates/atproto-identity/src/bin/atproto-identity-resolve.rs
src/bin/atproto-identity-sign.rs crates/atproto-identity/src/bin/atproto-identity-sign.rs
src/bin/atproto-identity-validate.rs crates/atproto-identity/src/bin/atproto-identity-validate.rs
src/config.rs crates/atproto-identity/src/config.rs
src/errors.rs crates/atproto-identity/src/errors.rs
src/key.rs crates/atproto-identity/src/key.rs
-1
src/lib.rs crates/atproto-identity/src/lib.rs
··· 21 21 //! 22 22 23 23 #![warn(missing_docs)] 24 - #![warn(missing_doc_code_examples)] 25 24 26 25 pub mod config; 27 26 pub mod errors;
src/model.rs crates/atproto-identity/src/model.rs
src/plc.rs crates/atproto-identity/src/plc.rs
src/resolve.rs crates/atproto-identity/src/resolve.rs
src/validation.rs crates/atproto-identity/src/validation.rs
src/web.rs crates/atproto-identity/src/web.rs