//! # atproto-plc //! //! Rust implementation of did:plc with WASM support for ATProto. //! //! ## Features //! //! - ✅ Validate did:plc identifiers //! - ✅ Parse and validate DID documents //! - ✅ Create new did:plc identities //! - ✅ Validate operation chains //! - ✅ Native Rust and WASM support //! - ✅ Recovery mechanism with 72-hour window //! //! ## Quick Start //! //! ### Rust //! //! ```rust //! use atproto_plc::{Did, DidBuilder, SigningKey, ServiceEndpoint}; //! //! // Validate a DID //! let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")?; //! //! // Create a new DID //! let rotation_key = SigningKey::generate_p256(); //! let signing_key = SigningKey::generate_k256(); //! //! let (did, operation, keys) = DidBuilder::new() //! .add_rotation_key(rotation_key) //! .add_verification_method("atproto".into(), signing_key) //! .add_also_known_as("at://alice.example.com".into()) //! .add_service( //! "atproto_pds".into(), //! ServiceEndpoint::new( //! "AtprotoPersonalDataServer".into(), //! "https://pds.example.com".into(), //! ), //! ) //! .build()?; //! //! println!("Created DID: {}", did); //! # Ok::<(), atproto_plc::PlcError>(()) //! ``` //! //! ## Specification //! //! This library implements the did:plc specification as defined at: //! //! //! ### DID Format //! //! A did:plc identifier consists of: //! - Prefix: "did:plc:" //! - Identifier: 24 lowercase base32 characters (alphabet: abcdefghijklmnopqrstuvwxyz234567) //! //! Example: `did:plc:ewvi7nxzyoun6zhxrhs64oiz` //! //! ### Key Points //! //! - **Rotation Keys**: 1-5 keys used to sign operations and recover control //! - **Verification Methods**: Up to 10 keys for authentication and signing //! - **Recovery Window**: 72 hours to recover control with higher-priority rotation keys //! - **Operation Size**: Maximum 7500 bytes per operation (DAG-CBOR encoded) //! //! ## Security Considerations //! //! ### Key Management //! //! - Private keys are zeroized from memory when dropped //! - Never compare ECDSA signatures directly - they are non-deterministic //! - Always use cryptographic verification functions //! //! ### Operation Signing //! //! - Operations are signed using DAG-CBOR encoding //! - Signatures use base64url encoding without padding //! - Both P-256 and secp256k1 curves are supported //! //! ## License //! //! Licensed under either of: //! //! - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) //! - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) //! //! at your option. #![warn(missing_docs)] // Core modules pub mod builder; pub mod crypto; pub mod did; pub mod document; pub mod encoding; pub mod error; pub mod operations; pub mod validation; // WASM bindings (only compiled for wasm32 target with "wasm" feature) #[cfg(all(target_arch = "wasm32", feature = "wasm"))] pub mod wasm; // Re-exports for convenience pub use builder::{BuilderKeys, DidBuilder}; pub use crypto::{SigningKey, VerifyingKey}; pub use did::Did; pub use document::{DidDocument, PlcState, Service, ServiceEndpoint, VerificationMethod}; pub use error::{PlcError, Result}; pub use operations::{Operation, UnsignedOperation}; pub use validation::OperationChainValidator; // Re-export WASM types when targeting wasm32 with "wasm" feature #[cfg(all(target_arch = "wasm32", feature = "wasm"))] pub use wasm::{ WasmDid, WasmDidBuilder, WasmDidDocument, WasmOperation, WasmServiceEndpoint, WasmSigningKey, WasmVerifyingKey, }; /// Library version pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Library name pub const NAME: &str = env!("CARGO_PKG_NAME"); /// Get library information pub fn library_info() -> String { format!("{} v{}", NAME, VERSION) } #[cfg(test)] mod tests { use super::*; #[test] fn test_library_info() { let info = library_info(); assert!(info.contains("atproto-plc")); assert!(info.contains("0.2.0")); } #[test] fn test_full_workflow() { // Create a new DID let rotation_key = SigningKey::generate_p256(); let signing_key = SigningKey::generate_k256(); let (did, operation, keys) = DidBuilder::new() .add_rotation_key(rotation_key) .add_verification_method("atproto".into(), signing_key) .add_also_known_as("at://alice.example.com".into()) .add_service( "atproto_pds".into(), ServiceEndpoint::new( "AtprotoPersonalDataServer".into(), "https://pds.example.com".into(), ), ) .build() .unwrap(); // Verify the DID format assert!(did.as_str().starts_with("did:plc:")); assert_eq!(did.identifier().len(), 24); // Verify the operation assert!(operation.is_genesis()); assert_eq!(operation.prev(), None); // Verify we got the keys back assert_eq!(keys.rotation_keys.len(), 1); assert_eq!(keys.verification_methods.len(), 1); // Validate the operation chain let state = OperationChainValidator::validate_chain(&[operation]).unwrap(); assert_eq!(state.rotation_keys.len(), 1); assert_eq!(state.verification_methods.len(), 1); assert_eq!(state.also_known_as.len(), 1); assert_eq!(state.services.len(), 1); // Convert to DID document let doc = state.to_did_document(&did); assert_eq!(doc.id, did); assert!(!doc.verification_method.is_empty()); assert!(!doc.service.is_empty()); } #[test] fn test_did_parsing() { // Valid DID let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); assert_eq!(did.identifier(), "ewvi7nxzyoun6zhxrhs64oiz"); // Invalid DIDs assert!(Did::parse("did:web:example.com").is_err()); assert!(Did::parse("did:plc:tooshort").is_err()); assert!(Did::parse("did:plc:UPPERCASE234567890123").is_err()); assert!(Did::parse("did:plc:0189abcd2345678901234567").is_err()); } #[test] fn test_crypto_roundtrip() { let key = SigningKey::generate_p256(); let data = b"hello world"; // Sign let signature = key.sign(data).unwrap(); // Verify with correct key let verifying_key = key.verifying_key(); assert!(verifying_key.verify(data, &signature).is_ok()); // Verify with wrong key should fail let wrong_key = SigningKey::generate_p256(); let wrong_verifying_key = wrong_key.verifying_key(); assert!(wrong_verifying_key.verify(data, &signature).is_err()); } #[test] fn test_did_key_roundtrip() { let key = SigningKey::generate_p256(); let did_key = key.to_did_key(); assert!(did_key.starts_with("did:key:z")); // Parse back let verifying_key = VerifyingKey::from_did_key(&did_key).unwrap(); assert_eq!(verifying_key, key.verifying_key()); } }