A library for ATProtocol identities.

feature: Adds the zeroize feature to project crates

Signed-off-by: Nick Gerakines <nick.gerakines@gmail.com>

Changed files
+171 -35
crates
atproto-identity
atproto-oauth
atproto-oauth-aip
atproto-oauth-axum
+12 -5
CLAUDE.md
··· 23 #### Identity Management 24 - **Resolve identities**: `cargo run --features clap --bin atproto-identity-resolve -- <handle_or_did>` 25 - **Resolve with DID document**: `cargo run --features clap --bin atproto-identity-resolve -- --did-document <handle_or_did>` 26 - - **Generate keys**: `cargo run --features clap --bin atproto-identity-key -- generate p256` 27 - **Sign data**: `cargo run --features clap --bin atproto-identity-sign -- <did_key> <json_file>` 28 - **Validate signatures**: `cargo run --features clap --bin atproto-identity-validate -- <did_key> <json_file> <signature>` 29 ··· 44 ## Architecture 45 46 A comprehensive Rust workspace with multiple crates: 47 - - **atproto-identity**: Core identity management with 8 modules (resolve, plc, web, model, validation, config, errors, key) 48 - **atproto-record**: Record signature operations and validation 49 - **atproto-client**: HTTP client with OAuth and identity integration 50 - **atproto-jetstream**: WebSocket event streaming with compression 51 - - **atproto-oauth**: OAuth workflow implementation with storage 52 - **atproto-oauth-axum**: Axum web framework integration for OAuth 53 - **atproto-xrpcs**: XRPC service framework 54 - **atproto-xrpcs-helloworld**: Complete example XRPC service ··· 59 - **Comprehensive error handling** with structured error types 60 - **Full test coverage** with unit tests across all modules 61 - **Modern dependencies** for HTTP, DNS, JSON, cryptographic operations, and WebSocket streaming 62 63 ## Error Handling 64 ··· 138 - **`src/validation.rs`**: Input validation for handles and DIDs 139 - **`src/config.rs`**: Configuration management and environment variable handling 140 - **`src/errors.rs`**: Structured error types following project conventions 141 - - **`src/key.rs`**: Cryptographic key operations including signature validation and key identification for P-256 and K-256 curves 142 143 ### CLI Tools (require --features clap) 144 145 #### Identity Management (atproto-identity) 146 - **`src/bin/atproto-identity-resolve.rs`**: Resolve AT Protocol handles and DIDs to canonical identifiers 147 - - **`src/bin/atproto-identity-key.rs`**: Generate and manage cryptographic keys (P-256, K-256) 148 - **`src/bin/atproto-identity-sign.rs`**: Create cryptographic signatures of JSON data 149 - **`src/bin/atproto-identity-validate.rs`**: Validate cryptographic signatures 150
··· 23 #### Identity Management 24 - **Resolve identities**: `cargo run --features clap --bin atproto-identity-resolve -- <handle_or_did>` 25 - **Resolve with DID document**: `cargo run --features clap --bin atproto-identity-resolve -- --did-document <handle_or_did>` 26 + - **Generate keys**: `cargo run --features clap --bin atproto-identity-key -- generate p256` (supports p256, p384, k256) 27 - **Sign data**: `cargo run --features clap --bin atproto-identity-sign -- <did_key> <json_file>` 28 - **Validate signatures**: `cargo run --features clap --bin atproto-identity-validate -- <did_key> <json_file> <signature>` 29 ··· 44 ## Architecture 45 46 A comprehensive Rust workspace with multiple crates: 47 + - **atproto-identity**: Core identity management with 11 modules (resolve, plc, web, model, validation, config, errors, key, storage, storage_lru, axum) 48 - **atproto-record**: Record signature operations and validation 49 - **atproto-client**: HTTP client with OAuth and identity integration 50 - **atproto-jetstream**: WebSocket event streaming with compression 51 + - **atproto-oauth**: OAuth workflow implementation with DPoP, PKCE, JWT, and storage abstractions 52 + - **atproto-oauth-aip**: AT Protocol OAuth AIP (Identity Provider) implementation with PAR support 53 - **atproto-oauth-axum**: Axum web framework integration for OAuth 54 - **atproto-xrpcs**: XRPC service framework 55 - **atproto-xrpcs-helloworld**: Complete example XRPC service ··· 60 - **Comprehensive error handling** with structured error types 61 - **Full test coverage** with unit tests across all modules 62 - **Modern dependencies** for HTTP, DNS, JSON, cryptographic operations, and WebSocket streaming 63 + - **Storage abstractions** with LRU caching support (optional via `lru` feature) 64 + - **Axum integration** for web framework support (optional via `axum` feature) 65 + - **Secure memory handling** (optional via `zeroize` feature) 66 67 ## Error Handling 68 ··· 142 - **`src/validation.rs`**: Input validation for handles and DIDs 143 - **`src/config.rs`**: Configuration management and environment variable handling 144 - **`src/errors.rs`**: Structured error types following project conventions 145 + - **`src/key.rs`**: Cryptographic key operations including signature validation and key identification for P-256, P-384, and K-256 curves 146 + - **`src/storage.rs`**: Storage abstraction interface for DID document caching 147 + - **`src/storage_lru.rs`**: LRU-based storage implementation (requires `lru` feature) 148 + - **`src/axum.rs`**: Axum web framework integration (requires `axum` feature) 149 150 ### CLI Tools (require --features clap) 151 152 #### Identity Management (atproto-identity) 153 - **`src/bin/atproto-identity-resolve.rs`**: Resolve AT Protocol handles and DIDs to canonical identifiers 154 + - **`src/bin/atproto-identity-key.rs`**: Generate and manage cryptographic keys (P-256, P-384, K-256) 155 - **`src/bin/atproto-identity-sign.rs`**: Create cryptographic signatures of JSON data 156 - **`src/bin/atproto-identity-validate.rs`**: Validate cryptographic signatures 157
+18
Cargo.lock
··· 153 "thiserror 2.0.12", 154 "tokio", 155 "tracing", 156 ] 157 158 [[package]] ··· 207 "tokio", 208 "tracing", 209 "ulid", 210 ] 211 212 [[package]] ··· 220 "serde", 221 "serde_json", 222 "thiserror 2.0.12", 223 ] 224 225 [[package]] ··· 249 "tokio", 250 "tracing", 251 "urlencoding", 252 ] 253 254 [[package]] ··· 3397 version = "1.8.1" 3398 source = "registry+https://github.com/rust-lang/crates.io-index" 3399 checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 3400 3401 [[package]] 3402 name = "zerotrie"
··· 153 "thiserror 2.0.12", 154 "tokio", 155 "tracing", 156 + "zeroize", 157 ] 158 159 [[package]] ··· 208 "tokio", 209 "tracing", 210 "ulid", 211 + "zeroize", 212 ] 213 214 [[package]] ··· 222 "serde", 223 "serde_json", 224 "thiserror 2.0.12", 225 + "zeroize", 226 ] 227 228 [[package]] ··· 252 "tokio", 253 "tracing", 254 "urlencoding", 255 + "zeroize", 256 ] 257 258 [[package]] ··· 3401 version = "1.8.1" 3402 source = "registry+https://github.com/rust-lang/crates.io-index" 3403 checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 3404 + dependencies = [ 3405 + "zeroize_derive", 3406 + ] 3407 + 3408 + [[package]] 3409 + name = "zeroize_derive" 3410 + version = "1.4.2" 3411 + source = "registry+https://github.com/rust-lang/crates.io-index" 3412 + checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 3413 + dependencies = [ 3414 + "proc-macro2", 3415 + "quote", 3416 + "syn", 3417 + ] 3418 3419 [[package]] 3420 name = "zerotrie"
+2
Cargo.toml
··· 65 urlencoding = "2.1" 66 zstd = "0.13" 67 68 [workspace.lints.rust] 69 unsafe_code = "forbid"
··· 65 urlencoding = "2.1" 66 zstd = "0.13" 67 68 + zeroize = { version = "1.8.1", features = ["zeroize_derive"] } 69 + 70 [workspace.lints.rust] 71 unsafe_code = "forbid"
+1 -1
Dockerfile
··· 24 # - atproto-oauth-axum: 1 binary (oauth-tool) 25 # - atproto-xrpcs-helloworld: 1 binary (xrpcs-helloworld) 26 # - atproto-jetstream: 1 binary (jetstream-consumer) 27 - RUN cargo build --release --bins -F clap 28 29 # Runtime stage - use distroless for minimal attack surface 30 FROM gcr.io/distroless/cc-debian12
··· 24 # - atproto-oauth-axum: 1 binary (oauth-tool) 25 # - atproto-xrpcs-helloworld: 1 binary (xrpcs-helloworld) 26 # - atproto-jetstream: 1 binary (jetstream-consumer) 27 + RUN cargo build --release --bins -F clap,zeroize 28 29 # Runtime stage - use distroless for minimal attack surface 30 FROM gcr.io/distroless/cc-debian12
+3
crates/atproto-identity/Cargo.toml
··· 66 axum = { version = "0.8", optional = true, features = ["macros"] } 67 http = { version = "1.0.0", optional = true } 68 69 [features] 70 default = ["lru", "axum"] 71 lru = ["dep:lru"] 72 axum = ["dep:axum", "dep:http"] 73 clap = ["dep:clap"] 74 75 [lints] 76 workspace = true
··· 66 axum = { version = "0.8", optional = true, features = ["macros"] } 67 http = { version = "1.0.0", optional = true } 68 69 + zeroize = { workspace = true, optional = true } 70 + 71 [features] 72 default = ["lru", "axum"] 73 lru = ["dep:lru"] 74 axum = ["dep:axum", "dep:http"] 75 clap = ["dep:clap"] 76 + zeroize = ["dep:zeroize"] 77 78 [lints] 79 workspace = true
+7 -2
crates/atproto-identity/src/key.rs
··· 60 61 use crate::errors::KeyError; 62 63 /// Cryptographic key types supported for AT Protocol identity. 64 #[cfg_attr(debug_assertions, derive(Debug))] 65 - #[derive(Clone, PartialEq)] 66 pub enum KeyType { 67 /// A p256 (P-256 / secp256r1 / ES256) public key. 68 /// The multibase / multicodec prefix is 8024. ··· 114 /// * `private_dpop_key_data` 115 /// 116 #[derive(Clone)] 117 pub struct KeyData(pub KeyType, pub Vec<u8>); 118 119 impl KeyData { ··· 134 135 /// Consumes self and returns the key type and bytes as a tuple. 136 pub fn into_parts(self) -> (KeyType, Vec<u8>) { 137 - (self.0, self.1) 138 } 139 } 140
··· 60 61 use crate::errors::KeyError; 62 63 + #[cfg(feature = "zeroize")] 64 + use zeroize::{Zeroize, ZeroizeOnDrop}; 65 + 66 /// Cryptographic key types supported for AT Protocol identity. 67 + #[derive(Clone, PartialEq)] 68 #[cfg_attr(debug_assertions, derive(Debug))] 69 + #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 70 pub enum KeyType { 71 /// A p256 (P-256 / secp256r1 / ES256) public key. 72 /// The multibase / multicodec prefix is 8024. ··· 118 /// * `private_dpop_key_data` 119 /// 120 #[derive(Clone)] 121 + #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] 122 pub struct KeyData(pub KeyType, pub Vec<u8>); 123 124 impl KeyData { ··· 139 140 /// Consumes self and returns the key type and bytes as a tuple. 141 pub fn into_parts(self) -> (KeyType, Vec<u8>) { 142 + (self.0.clone(), self.1.clone()) 143 } 144 } 145
+5
crates/atproto-oauth-aip/Cargo.toml
··· 24 serde.workspace = true 25 thiserror.workspace = true 26 27 [lints] 28 workspace = true
··· 24 serde.workspace = true 25 thiserror.workspace = true 26 27 + zeroize = { workspace = true, optional = true } 28 + 29 + [features] 30 + zeroize = ["dep:zeroize", "atproto-oauth/zeroize"] 31 + 32 [lints] 33 workspace = true
+28 -4
crates/atproto-oauth-aip/src/workflow.rs
··· 117 //! for each phase of the OAuth flow including network failures, parsing errors, 118 //! and protocol violations. 119 120 - use crate::errors::OAuthWorkflowError; 121 use anyhow::Result; 122 use atproto_oauth::{ 123 resources::{AuthorizationServer, OAuthProtectedResource}, ··· 125 }; 126 use serde::Deserialize; 127 128 /// OAuth client configuration containing essential client credentials. 129 pub struct OAuthClient { 130 /// The redirect URI where the authorization server will send the user after authorization. 131 pub redirect_uri: String, 132 /// The unique client identifier for this OAuth client. 133 pub client_id: String, 134 135 /// The client secret used for authenticating with the authorization server. ··· 151 /// This structure contains all the information needed to make authenticated 152 /// requests to AT Protocol services after a successful OAuth flow. 153 #[derive(Clone, Deserialize)] 154 pub struct ATProtocolSession { 155 /// The Decentralized Identifier (DID) of the authenticated user. 156 pub did: String, 157 /// The handle (username) of the authenticated user. 158 pub handle: String, 159 /// The OAuth access token for making authenticated requests. 160 pub access_token: String, 161 /// The type of token (typically "Bearer"). 162 pub token_type: String, 163 /// The list of OAuth scopes granted to this session. 164 pub scopes: Vec<String>, 165 /// The Personal Data Server (PDS) endpoint URL for this user. 166 pub pds_endpoint: String, 167 /// The DPoP (Demonstration of Proof-of-Possession) key in JWK format. 168 pub dpop_key: String, 169 /// Unix timestamp indicating when this session expires. 170 pub expires_at: i64, 171 } 172 173 #[derive(Deserialize, Clone)] 174 #[serde(untagged)] 175 enum WrappedATProtocolSession { 176 ATProtocolSession(ATProtocolSession), 177 Error { 178 error: String, 179 error_description: Option<String>, ··· 411 .map_err(OAuthWorkflowError::SessionResponseParseFailed)?; 412 413 match response { 414 - WrappedATProtocolSession::ATProtocolSession(value) => Ok(value), 415 WrappedATProtocolSession::Error { 416 - error, 417 - error_description, 418 } => { 419 let error_message = if let Some(value) = error_description { 420 format!("{error}: {value}")
··· 117 //! for each phase of the OAuth flow including network failures, parsing errors, 118 //! and protocol violations. 119 120 use anyhow::Result; 121 use atproto_oauth::{ 122 resources::{AuthorizationServer, OAuthProtectedResource}, ··· 124 }; 125 use serde::Deserialize; 126 127 + use crate::errors::OAuthWorkflowError; 128 + 129 + #[cfg(feature = "zeroize")] 130 + use zeroize::{Zeroize, ZeroizeOnDrop}; 131 + 132 /// OAuth client configuration containing essential client credentials. 133 + #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 134 pub struct OAuthClient { 135 /// The redirect URI where the authorization server will send the user after authorization. 136 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 137 pub redirect_uri: String, 138 + 139 /// The unique client identifier for this OAuth client. 140 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 141 pub client_id: String, 142 143 /// The client secret used for authenticating with the authorization server. ··· 159 /// This structure contains all the information needed to make authenticated 160 /// requests to AT Protocol services after a successful OAuth flow. 161 #[derive(Clone, Deserialize)] 162 + #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 163 pub struct ATProtocolSession { 164 /// The Decentralized Identifier (DID) of the authenticated user. 165 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 166 pub did: String, 167 + 168 /// The handle (username) of the authenticated user. 169 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 170 pub handle: String, 171 + 172 /// The OAuth access token for making authenticated requests. 173 pub access_token: String, 174 + 175 /// The type of token (typically "Bearer"). 176 pub token_type: String, 177 + 178 /// The list of OAuth scopes granted to this session. 179 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 180 pub scopes: Vec<String>, 181 + 182 /// The Personal Data Server (PDS) endpoint URL for this user. 183 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 184 pub pds_endpoint: String, 185 + 186 /// The DPoP (Demonstration of Proof-of-Possession) key in JWK format. 187 pub dpop_key: String, 188 + 189 /// Unix timestamp indicating when this session expires. 190 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 191 pub expires_at: i64, 192 } 193 194 #[derive(Deserialize, Clone)] 195 + #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 196 #[serde(untagged)] 197 enum WrappedATProtocolSession { 198 ATProtocolSession(ATProtocolSession), 199 + 200 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 201 Error { 202 error: String, 203 error_description: Option<String>, ··· 435 .map_err(OAuthWorkflowError::SessionResponseParseFailed)?; 436 437 match response { 438 + WrappedATProtocolSession::ATProtocolSession(ref value) => Ok(value.clone()), 439 WrappedATProtocolSession::Error { 440 + ref error, 441 + ref error_description, 442 } => { 443 let error_message = if let Some(value) = error_description { 444 format!("{error}: {value}")
+3
crates/atproto-oauth-axum/Cargo.toml
··· 47 rpassword = { workspace = true, optional = true } 48 secrecy = { workspace = true, optional = true } 49 50 [features] 51 clap = ["dep:clap", "dep:rpassword", "dep:secrecy"] 52 53 [lints] 54 workspace = true
··· 47 rpassword = { workspace = true, optional = true } 48 secrecy = { workspace = true, optional = true } 49 50 + zeroize = { workspace = true, optional = true } 51 + 52 [features] 53 clap = ["dep:clap", "dep:rpassword", "dep:secrecy"] 54 + zeroize = ["dep:zeroize", "atproto-identity/zeroize", "atproto-oauth/zeroize"] 55 56 [lints] 57 workspace = true
+1
crates/atproto-oauth-axum/src/bin/atproto-oauth-tool.rs
··· 268 .iter() 269 .filter_map(|value| to_public(value).ok()) 270 .collect(), 271 client_name: None, 272 client_uri: None, 273 logo_uri: None,
··· 268 .iter() 269 .filter_map(|value| to_public(value).ok()) 270 .collect(), 271 + scope: None, 272 client_name: None, 273 client_uri: None, 274 logo_uri: None,
+3 -3
crates/atproto-oauth-axum/src/handle_complete.rs
··· 92 let private_dpop_key_data = identify_key(&oauth_request.dpop_private_key)?; 93 94 let oauth_client = OAuthClient { 95 - redirect_uri: oauth_client_config.redirect_uris, 96 - client_id: oauth_client_config.client_id, 97 private_signing_key_data, 98 }; 99 ··· 127 token_response.token_type, 128 token_response.expires_in, 129 token_response.scope, 130 - token_response.sub.unwrap_or("unknown".to_string()), 131 private_dpop_key_data 132 ); 133
··· 92 let private_dpop_key_data = identify_key(&oauth_request.dpop_private_key)?; 93 94 let oauth_client = OAuthClient { 95 + redirect_uri: oauth_client_config.redirect_uris.clone(), 96 + client_id: oauth_client_config.client_id.clone(), 97 private_signing_key_data, 98 }; 99 ··· 127 token_response.token_type, 128 token_response.expires_in, 129 token_response.scope, 130 + token_response.sub.clone().unwrap_or("unknown".to_string()), 131 private_dpop_key_data 132 ); 133
-4
crates/atproto-oauth-axum/src/handle_init.rs
··· 1 - //! OAuth authorization initiation handler (placeholder). 2 - //! 3 - //! Reserved module for OAuth authorization initiation endpoints. 4 - //! Currently unused but available for future implementation.
···
-1
crates/atproto-oauth-axum/src/lib.rs
··· 35 36 pub mod errors; 37 pub mod handle_complete; 38 - pub mod handle_init; 39 pub mod handle_jwks; 40 pub mod handler_metadata; 41 pub mod state;
··· 35 36 pub mod errors; 37 pub mod handle_complete; 38 pub mod handle_jwks; 39 pub mod handler_metadata; 40 pub mod state;
+21
crates/atproto-oauth-axum/src/state.rs
··· 8 use http::request::Parts; 9 use std::convert::Infallible; 10 11 /// OAuth client configuration for Axum handlers. 12 /// 13 /// Contains the essential configuration needed for OAuth client operations. 14 #[derive(Clone, Default)] 15 pub struct OAuthClientConfig { 16 /// OAuth client identifier 17 pub client_id: String, 18 /// Allowed OAuth redirect URIs 19 pub redirect_uris: String, 20 /// JSON Web Key Set URI for public keys 21 pub jwks_uri: Option<String>, 22 /// Signing keys for JWT operations 23 pub signing_keys: Vec<KeyData>, 24 /// OAuth scope, defaults to "atproto transition:generic" 25 pub scope: Option<String>, 26 27 /// Optional human-readable client name 28 pub client_name: Option<String>, 29 /// Optional client website URI 30 pub client_uri: Option<String>, 31 /// Optional client logo URI 32 pub logo_uri: Option<String>, 33 /// Optional terms of service URI 34 pub tos_uri: Option<String>, 35 /// Optional privacy policy URI 36 pub policy_uri: Option<String>, 37 } 38
··· 8 use http::request::Parts; 9 use std::convert::Infallible; 10 11 + #[cfg(feature = "zeroize")] 12 + use zeroize::{Zeroize, ZeroizeOnDrop}; 13 + 14 /// OAuth client configuration for Axum handlers. 15 /// 16 /// Contains the essential configuration needed for OAuth client operations. 17 #[derive(Clone, Default)] 18 + #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 19 pub struct OAuthClientConfig { 20 /// OAuth client identifier 21 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 22 pub client_id: String, 23 + 24 /// Allowed OAuth redirect URIs 25 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 26 pub redirect_uris: String, 27 + 28 /// JSON Web Key Set URI for public keys 29 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 30 pub jwks_uri: Option<String>, 31 + 32 /// Signing keys for JWT operations 33 pub signing_keys: Vec<KeyData>, 34 + 35 /// OAuth scope, defaults to "atproto transition:generic" 36 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 37 pub scope: Option<String>, 38 39 /// Optional human-readable client name 40 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 41 pub client_name: Option<String>, 42 + 43 /// Optional client website URI 44 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 45 pub client_uri: Option<String>, 46 + 47 /// Optional client logo URI 48 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 49 pub logo_uri: Option<String>, 50 + 51 /// Optional terms of service URI 52 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 53 pub tos_uri: Option<String>, 54 + 55 /// Optional privacy policy URI 56 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 57 pub policy_uri: Option<String>, 58 } 59
+3
crates/atproto-oauth/Cargo.toml
··· 44 axum = { version = "0.8", optional = true } 45 http = { version = "1.0.0", optional = true } 46 47 [features] 48 default = ["lru", "axum"] 49 lru = ["dep:lru"] 50 axum = ["dep:axum", "dep:http"] 51 52 [lints] 53 workspace = true
··· 44 axum = { version = "0.8", optional = true } 45 http = { version = "1.0.0", optional = true } 46 47 + zeroize = { workspace = true, optional = true } 48 + 49 [features] 50 default = ["lru", "axum"] 51 lru = ["dep:lru"] 52 axum = ["dep:axum", "dep:http"] 53 + zeroize = ["dep:zeroize", "atproto-identity/zeroize"] 54 55 [lints] 56 workspace = true
+2 -2
crates/atproto-oauth/src/dpop.rs
··· 342 type_: Some("dpop+jwt".to_string()), 343 algorithm: Some("ES256".to_string()), 344 json_web_key: Some(dpop_jwk), 345 - ..Default::default() 346 }; 347 348 let auth = access_token.map(challenge); ··· 1252 type_: Some("dpop+jwt".to_string()), 1253 algorithm: Some("ES256".to_string()), 1254 json_web_key: Some(dpop_jwk), 1255 - ..Default::default() 1256 }; 1257 1258 let claims = Claims::new(JoseClaims {
··· 342 type_: Some("dpop+jwt".to_string()), 343 algorithm: Some("ES256".to_string()), 344 json_web_key: Some(dpop_jwk), 345 + key_id: None, 346 }; 347 348 let auth = access_token.map(challenge); ··· 1252 type_: Some("dpop+jwt".to_string()), 1253 algorithm: Some("ES256".to_string()), 1254 json_web_key: Some(dpop_jwk), 1255 + key_id: None, 1256 }; 1257 1258 let claims = Claims::new(JoseClaims {
+8 -1
crates/atproto-oauth/src/jwk.rs
··· 15 16 use crate::errors::JWKError; 17 18 /// A wrapped JSON Web Key with additional metadata. 19 #[cfg_attr(debug_assertions, derive(Debug))] 20 - #[derive(Serialize, Deserialize, Clone, PartialEq)] 21 pub struct WrappedJsonWebKey { 22 /// Key identifier (kid) for the JWK. 23 #[serde(skip_serializing_if = "Option::is_none", default)] 24 pub kid: Option<String>, 25 26 /// Algorithm (alg) used with this key. 27 #[serde(skip_serializing_if = "Option::is_none", default)] 28 pub alg: Option<String>, 29 30 /// Public key use (use) parameter, typically "sig" for signature operations. 31 #[serde(rename = "use", skip_serializing_if = "Option::is_none")] 32 pub _use: Option<String>, 33 34 /// The underlying elliptic curve JWK.
··· 15 16 use crate::errors::JWKError; 17 18 + #[cfg(feature = "zeroize")] 19 + use zeroize::{Zeroize, ZeroizeOnDrop}; 20 + 21 /// A wrapped JSON Web Key with additional metadata. 22 + #[derive(Serialize, Deserialize, Clone, PartialEq)] 23 #[cfg_attr(debug_assertions, derive(Debug))] 24 + #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 25 pub struct WrappedJsonWebKey { 26 /// Key identifier (kid) for the JWK. 27 #[serde(skip_serializing_if = "Option::is_none", default)] 28 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 29 pub kid: Option<String>, 30 31 /// Algorithm (alg) used with this key. 32 #[serde(skip_serializing_if = "Option::is_none", default)] 33 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 34 pub alg: Option<String>, 35 36 /// Public key use (use) parameter, typically "sig" for signature operations. 37 #[serde(rename = "use", skip_serializing_if = "Option::is_none")] 38 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 39 pub _use: Option<String>, 40 41 /// The underlying elliptic curve JWK.
+12 -4
crates/atproto-oauth/src/jwt.rs
··· 4 //! custom extensions for AT Protocol OAuth flows. Supports ES256, ES384, and ES256K elliptic curve 5 //! signature algorithms with comprehensive timestamp validation and structured error handling. 6 7 - use crate::encoding::ToBase64; 8 - use crate::errors::JWTError; 9 use anyhow::Result; 10 use atproto_identity::key::{KeyData, KeyType, sign, to_public, validate}; 11 use base64::{Engine as _, engine::general_purpose}; ··· 14 use std::collections::BTreeMap; 15 use std::time::{SystemTime, UNIX_EPOCH}; 16 17 /// JWT header containing algorithm and key metadata. 18 #[cfg_attr(debug_assertions, derive(Debug))] 19 - #[derive(Clone, Default, PartialEq, Serialize, Deserialize)] 20 pub struct Header { 21 /// Algorithm used for signing (e.g., "ES256", "ES384", "ES256K"). 22 #[serde(rename = "alg", skip_serializing_if = "Option::is_none")] ··· 175 (Some("ES384"), KeyType::P384Private) | (Some("ES384"), KeyType::P384Public) => {} 176 _ => { 177 return Err(JWTError::UnsupportedAlgorithm { 178 - algorithm: header.algorithm.unwrap_or_else(|| "none".to_string()), 179 key_type: format!("{}", key_data.key_type()), 180 } 181 .into());
··· 4 //! custom extensions for AT Protocol OAuth flows. Supports ES256, ES384, and ES256K elliptic curve 5 //! signature algorithms with comprehensive timestamp validation and structured error handling. 6 7 use anyhow::Result; 8 use atproto_identity::key::{KeyData, KeyType, sign, to_public, validate}; 9 use base64::{Engine as _, engine::general_purpose}; ··· 12 use std::collections::BTreeMap; 13 use std::time::{SystemTime, UNIX_EPOCH}; 14 15 + use crate::encoding::ToBase64; 16 + use crate::errors::JWTError; 17 + 18 + #[cfg(feature = "zeroize")] 19 + use zeroize::{Zeroize, ZeroizeOnDrop}; 20 + 21 /// JWT header containing algorithm and key metadata. 22 + #[derive(Clone, Default, PartialEq, Serialize, Deserialize)] 23 #[cfg_attr(debug_assertions, derive(Debug))] 24 + #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 25 pub struct Header { 26 /// Algorithm used for signing (e.g., "ES256", "ES384", "ES256K"). 27 #[serde(rename = "alg", skip_serializing_if = "Option::is_none")] ··· 180 (Some("ES384"), KeyType::P384Private) | (Some("ES384"), KeyType::P384Public) => {} 181 _ => { 182 return Err(JWTError::UnsupportedAlgorithm { 183 + algorithm: header 184 + .algorithm 185 + .clone() 186 + .unwrap_or_else(|| "none".to_string()), 187 key_type: format!("{}", key_data.key_type()), 188 } 189 .into());
+42 -8
crates/atproto-oauth/src/workflow.rs
··· 76 //! ).await?; 77 //! ``` 78 79 - use crate::{ 80 - dpop::{DpopRetry, auth_dpop}, 81 - errors::OAuthClientError, 82 - jwt::{Claims, Header, JoseClaims, mint}, 83 - resources::{AuthorizationServer, pds_resources}, 84 - }; 85 use atproto_identity::key::KeyData; 86 use chrono::{DateTime, Utc}; 87 use rand::distributions::{Alphanumeric, DistString}; 88 use reqwest_chain::ChainMiddleware; 89 use reqwest_middleware::ClientBuilder; 90 - 91 use std::collections::HashMap; 92 93 - use serde::Deserialize; 94 95 /// Response from a Pushed Authorization Request (PAR) endpoint. 96 /// ··· 127 /// 128 /// This struct holds the client configuration needed for OAuth authorization flows, 129 /// including the redirect URI, client identifier, and signing key. 130 pub struct OAuthClient { 131 /// The redirect URI where the authorization server will send the user after authorization. 132 pub redirect_uri: String, 133 /// The unique client identifier for this OAuth client. 134 pub client_id: String, 135 /// The private key data used for signing client assertions. 136 pub private_signing_key_data: KeyData, 137 } ··· 141 /// This struct contains all the necessary information to track and complete 142 /// an OAuth authorization request, including security parameters and timing. 143 #[derive(Clone, PartialEq)] 144 pub struct OAuthRequest { 145 /// The OAuth state parameter used to prevent CSRF attacks. 146 pub oauth_state: String, 147 /// The authorization server issuer identifier. 148 pub issuer: String, 149 /// The DID (Decentralized Identifier) of the user. 150 pub did: String, 151 /// The nonce value for additional security. 152 pub nonce: String, 153 /// The PKCE code verifier for this authorization request. 154 pub pkce_verifier: String, 155 /// The public key used for signing (serialized). 156 pub signing_public_key: String, 157 /// The DPoP private key (serialized). 158 pub dpop_private_key: String, 159 /// When this OAuth request was created. 160 pub created_at: DateTime<Utc>, 161 /// When this OAuth request expires. 162 pub expires_at: DateTime<Utc>, 163 } 164 ··· 183 /// This struct represents the successful response from an OAuth token exchange, 184 /// containing the access token and related metadata. 185 #[derive(Clone, Deserialize)] 186 pub struct TokenResponse { 187 /// The access token that can be used to access protected resources. 188 pub access_token: String, 189 /// The type of token, typically "Bearer" or "DPoP". 190 pub token_type: String, 191 /// The refresh token that can be used to obtain new access tokens. 192 pub refresh_token: String, 193 /// The scope of access granted by the access token. 194 pub scope: String, 195 /// The lifetime of the access token in seconds. 196 pub expires_in: u32, 197 /// The subject identifier (usually the user's DID). 198 pub sub: Option<String>, 199 200 /// Additional fields returned by the authorization server. 201 #[serde(flatten)] 202 pub extra: HashMap<String, serde_json::Value>, 203 } 204
··· 76 //! ).await?; 77 //! ``` 78 79 use atproto_identity::key::KeyData; 80 use chrono::{DateTime, Utc}; 81 use rand::distributions::{Alphanumeric, DistString}; 82 use reqwest_chain::ChainMiddleware; 83 use reqwest_middleware::ClientBuilder; 84 + use serde::Deserialize; 85 use std::collections::HashMap; 86 87 + #[cfg(feature = "zeroize")] 88 + use zeroize::{Zeroize, ZeroizeOnDrop}; 89 + 90 + use crate::{ 91 + dpop::{DpopRetry, auth_dpop}, 92 + errors::OAuthClientError, 93 + jwt::{Claims, Header, JoseClaims, mint}, 94 + resources::{AuthorizationServer, pds_resources}, 95 + }; 96 97 /// Response from a Pushed Authorization Request (PAR) endpoint. 98 /// ··· 129 /// 130 /// This struct holds the client configuration needed for OAuth authorization flows, 131 /// including the redirect URI, client identifier, and signing key. 132 + #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 133 pub struct OAuthClient { 134 /// The redirect URI where the authorization server will send the user after authorization. 135 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 136 pub redirect_uri: String, 137 + 138 /// The unique client identifier for this OAuth client. 139 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 140 pub client_id: String, 141 + 142 /// The private key data used for signing client assertions. 143 pub private_signing_key_data: KeyData, 144 } ··· 148 /// This struct contains all the necessary information to track and complete 149 /// an OAuth authorization request, including security parameters and timing. 150 #[derive(Clone, PartialEq)] 151 + #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 152 pub struct OAuthRequest { 153 /// The OAuth state parameter used to prevent CSRF attacks. 154 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 155 pub oauth_state: String, 156 + 157 /// The authorization server issuer identifier. 158 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 159 pub issuer: String, 160 + 161 /// The DID (Decentralized Identifier) of the user. 162 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 163 pub did: String, 164 + 165 /// The nonce value for additional security. 166 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 167 pub nonce: String, 168 + 169 /// The PKCE code verifier for this authorization request. 170 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 171 pub pkce_verifier: String, 172 + 173 /// The public key used for signing (serialized). 174 pub signing_public_key: String, 175 + 176 /// The DPoP private key (serialized). 177 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 178 pub dpop_private_key: String, 179 + 180 /// When this OAuth request was created. 181 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 182 pub created_at: DateTime<Utc>, 183 + 184 /// When this OAuth request expires. 185 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 186 pub expires_at: DateTime<Utc>, 187 } 188 ··· 207 /// This struct represents the successful response from an OAuth token exchange, 208 /// containing the access token and related metadata. 209 #[derive(Clone, Deserialize)] 210 + #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 211 pub struct TokenResponse { 212 /// The access token that can be used to access protected resources. 213 pub access_token: String, 214 + 215 /// The type of token, typically "Bearer" or "DPoP". 216 pub token_type: String, 217 + 218 /// The refresh token that can be used to obtain new access tokens. 219 pub refresh_token: String, 220 + 221 /// The scope of access granted by the access token. 222 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 223 pub scope: String, 224 + 225 /// The lifetime of the access token in seconds. 226 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 227 pub expires_in: u32, 228 + 229 /// The subject identifier (usually the user's DID). 230 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 231 pub sub: Option<String>, 232 233 /// Additional fields returned by the authorization server. 234 #[serde(flatten)] 235 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 236 pub extra: HashMap<String, serde_json::Value>, 237 } 238