Rust and WASM did-method-plc tools and structures
at main 7.2 kB view raw
1//! # atproto-plc 2//! 3//! Rust implementation of did:plc with WASM support for ATProto. 4//! 5//! ## Features 6//! 7//! - ✅ Validate did:plc identifiers 8//! - ✅ Parse and validate DID documents 9//! - ✅ Create new did:plc identities 10//! - ✅ Validate operation chains 11//! - ✅ Native Rust and WASM support 12//! - ✅ Recovery mechanism with 72-hour window 13//! 14//! ## Quick Start 15//! 16//! ### Rust 17//! 18//! ```rust 19//! use atproto_plc::{Did, DidBuilder, SigningKey, ServiceEndpoint}; 20//! 21//! // Validate a DID 22//! let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")?; 23//! 24//! // Create a new DID 25//! let rotation_key = SigningKey::generate_p256(); 26//! let signing_key = SigningKey::generate_k256(); 27//! 28//! let (did, operation, keys) = DidBuilder::new() 29//! .add_rotation_key(rotation_key) 30//! .add_verification_method("atproto".into(), signing_key) 31//! .add_also_known_as("at://alice.example.com".into()) 32//! .add_service( 33//! "atproto_pds".into(), 34//! ServiceEndpoint::new( 35//! "AtprotoPersonalDataServer".into(), 36//! "https://pds.example.com".into(), 37//! ), 38//! ) 39//! .build()?; 40//! 41//! println!("Created DID: {}", did); 42//! # Ok::<(), atproto_plc::PlcError>(()) 43//! ``` 44//! 45//! ## Specification 46//! 47//! This library implements the did:plc specification as defined at: 48//! <https://web.plc.directory/spec/v0.1/did-plc> 49//! 50//! ### DID Format 51//! 52//! A did:plc identifier consists of: 53//! - Prefix: "did:plc:" 54//! - Identifier: 24 lowercase base32 characters (alphabet: abcdefghijklmnopqrstuvwxyz234567) 55//! 56//! Example: `did:plc:ewvi7nxzyoun6zhxrhs64oiz` 57//! 58//! ### Key Points 59//! 60//! - **Rotation Keys**: 1-5 keys used to sign operations and recover control 61//! - **Verification Methods**: Up to 10 keys for authentication and signing 62//! - **Recovery Window**: 72 hours to recover control with higher-priority rotation keys 63//! - **Operation Size**: Maximum 7500 bytes per operation (DAG-CBOR encoded) 64//! 65//! ## Security Considerations 66//! 67//! ### Key Management 68//! 69//! - Private keys are zeroized from memory when dropped 70//! - Never compare ECDSA signatures directly - they are non-deterministic 71//! - Always use cryptographic verification functions 72//! 73//! ### Operation Signing 74//! 75//! - Operations are signed using DAG-CBOR encoding 76//! - Signatures use base64url encoding without padding 77//! - Both P-256 and secp256k1 curves are supported 78//! 79//! ## License 80//! 81//! Licensed under either of: 82//! 83//! - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>) 84//! - MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>) 85//! 86//! at your option. 87 88#![warn(missing_docs)] 89 90// Core modules 91pub mod builder; 92pub mod crypto; 93pub mod did; 94pub mod document; 95pub mod encoding; 96pub mod error; 97pub mod operations; 98pub mod validation; 99 100// WASM bindings (only compiled for wasm32 target with "wasm" feature) 101#[cfg(all(target_arch = "wasm32", feature = "wasm"))] 102pub mod wasm; 103 104// Re-exports for convenience 105pub use builder::{BuilderKeys, DidBuilder}; 106pub use crypto::{SigningKey, VerifyingKey}; 107pub use did::Did; 108pub use document::{DidDocument, PlcState, Service, ServiceEndpoint, VerificationMethod}; 109pub use error::{PlcError, Result}; 110pub use operations::{Operation, UnsignedOperation}; 111pub use validation::OperationChainValidator; 112 113// Re-export WASM types when targeting wasm32 with "wasm" feature 114#[cfg(all(target_arch = "wasm32", feature = "wasm"))] 115pub use wasm::{ 116 WasmDid, WasmDidBuilder, WasmDidDocument, WasmOperation, WasmServiceEndpoint, WasmSigningKey, 117 WasmVerifyingKey, 118}; 119 120/// Library version 121pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 122 123/// Library name 124pub const NAME: &str = env!("CARGO_PKG_NAME"); 125 126/// Get library information 127pub fn library_info() -> String { 128 format!("{} v{}", NAME, VERSION) 129} 130 131#[cfg(test)] 132mod tests { 133 use super::*; 134 135 #[test] 136 fn test_library_info() { 137 let info = library_info(); 138 assert!(info.contains("atproto-plc")); 139 assert!(info.contains("0.2.0")); 140 } 141 142 #[test] 143 fn test_full_workflow() { 144 // Create a new DID 145 let rotation_key = SigningKey::generate_p256(); 146 let signing_key = SigningKey::generate_k256(); 147 148 let (did, operation, keys) = DidBuilder::new() 149 .add_rotation_key(rotation_key) 150 .add_verification_method("atproto".into(), signing_key) 151 .add_also_known_as("at://alice.example.com".into()) 152 .add_service( 153 "atproto_pds".into(), 154 ServiceEndpoint::new( 155 "AtprotoPersonalDataServer".into(), 156 "https://pds.example.com".into(), 157 ), 158 ) 159 .build() 160 .unwrap(); 161 162 // Verify the DID format 163 assert!(did.as_str().starts_with("did:plc:")); 164 assert_eq!(did.identifier().len(), 24); 165 166 // Verify the operation 167 assert!(operation.is_genesis()); 168 assert_eq!(operation.prev(), None); 169 170 // Verify we got the keys back 171 assert_eq!(keys.rotation_keys.len(), 1); 172 assert_eq!(keys.verification_methods.len(), 1); 173 174 // Validate the operation chain 175 let state = OperationChainValidator::validate_chain(&[operation]).unwrap(); 176 assert_eq!(state.rotation_keys.len(), 1); 177 assert_eq!(state.verification_methods.len(), 1); 178 assert_eq!(state.also_known_as.len(), 1); 179 assert_eq!(state.services.len(), 1); 180 181 // Convert to DID document 182 let doc = state.to_did_document(&did); 183 assert_eq!(doc.id, did); 184 assert!(!doc.verification_method.is_empty()); 185 assert!(!doc.service.is_empty()); 186 } 187 188 #[test] 189 fn test_did_parsing() { 190 // Valid DID 191 let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); 192 assert_eq!(did.identifier(), "ewvi7nxzyoun6zhxrhs64oiz"); 193 194 // Invalid DIDs 195 assert!(Did::parse("did:web:example.com").is_err()); 196 assert!(Did::parse("did:plc:tooshort").is_err()); 197 assert!(Did::parse("did:plc:UPPERCASE234567890123").is_err()); 198 assert!(Did::parse("did:plc:0189abcd2345678901234567").is_err()); 199 } 200 201 #[test] 202 fn test_crypto_roundtrip() { 203 let key = SigningKey::generate_p256(); 204 let data = b"hello world"; 205 206 // Sign 207 let signature = key.sign(data).unwrap(); 208 209 // Verify with correct key 210 let verifying_key = key.verifying_key(); 211 assert!(verifying_key.verify(data, &signature).is_ok()); 212 213 // Verify with wrong key should fail 214 let wrong_key = SigningKey::generate_p256(); 215 let wrong_verifying_key = wrong_key.verifying_key(); 216 assert!(wrong_verifying_key.verify(data, &signature).is_err()); 217 } 218 219 #[test] 220 fn test_did_key_roundtrip() { 221 let key = SigningKey::generate_p256(); 222 let did_key = key.to_did_key(); 223 224 assert!(did_key.starts_with("did:key:z")); 225 226 // Parse back 227 let verifying_key = VerifyingKey::from_did_key(&did_key).unwrap(); 228 assert_eq!(verifying_key, key.verifying_key()); 229 } 230}