Rust and WASM did-method-plc tools and structures
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}