blueprint_auth/
lib.rs

1//! Authentication module for the Blueprint SDK.
2//!
3//! This module provides a three-tier token authentication system:
4//!
5//! 1. **API Keys** (`ak_xxxxx.yyyyy`) - Long-lived credentials for service authentication
6//! 2. **Access Tokens** (`v4.local.xxxxx`) - Short-lived Paseto tokens for authorization
7//! 3. **Legacy Tokens** (`id|token`) - Deprecated format for backward compatibility
8//!
9//! # Architecture
10//!
11//! The authentication flow follows these steps:
12//! 1. Client authenticates with API key
13//! 2. API key is exchanged for a short-lived access token
14//! 3. Access token is used for subsequent requests
15//! 4. Token refresh happens automatically before expiration
16//!
17//! # Security Features
18//!
19//! - Cryptographic tenant binding prevents impersonation
20//! - Header re-validation prevents injection attacks  
21//! - Persistent key storage with secure permissions
22//! - Automatic token rotation and refresh
23//!
24//! # Example
25//!
26//! ```no_run
27//! use blueprint_auth::proxy::AuthenticatedProxy;
28//!
29//! fn main() -> Result<(), Box<dyn std::error::Error>> {
30//!     // Initialize proxy with persistent storage
31//!     let proxy = AuthenticatedProxy::new("/var/lib/auth/db")?;
32//!
33//!     // Start the proxy server
34//!     let router = proxy.router();
35//!     Ok(())
36//! }
37//! ```
38
39use blueprint_std::rand::{CryptoRng, Rng};
40
41/// Long-lived API key management
42pub mod api_keys;
43/// Generates API Tokens for the authentication process.
44pub mod api_tokens;
45/// Unified authentication token types
46pub mod auth_token;
47/// Certificate Authority utilities for mTLS
48pub mod certificate_authority;
49/// The database module for the authentication process.
50pub mod db;
51/// Database models
52pub mod models;
53/// OAuth 2.0 JWT assertion verifier and per-service policy
54pub mod oauth;
55/// Paseto token generation and validation
56pub mod paseto_tokens;
57/// Authenticated Proxy Server built on top of Axum.
58pub mod proxy;
59/// Request-level auth context parsing and extractors
60pub mod request_auth;
61/// Request extension plumbing for client certificate identity
62pub mod request_extensions;
63/// TLS assets management
64pub mod tls_assets;
65/// TLS client management for outbound connections
66pub mod tls_client;
67/// TLS envelope encryption for certificate material
68pub mod tls_envelope;
69/// TLS listener for dual socket support (HTTP + HTTPS)
70pub mod tls_listener;
71/// Holds the authentication-related types.
72pub mod types;
73/// Header validation utilities
74pub mod validation;
75
76#[cfg(test)]
77mod test_client;
78
79#[cfg(test)]
80mod tests;
81
82#[derive(Debug, thiserror::Error)]
83pub enum Error {
84    /// Error related to the ECDSA signature verification.
85    #[error("k256 error: {0}")]
86    K256(k256::ecdsa::Error),
87    /// Error related to the SR25519 signature verification.
88    #[error("Schnorrkel error: {0}")]
89    Schnorrkel(schnorrkel::SignatureError),
90
91    #[error(transparent)]
92    RocksDB(#[from] rocksdb::Error),
93
94    #[error("Invalid database compaction style: {0}")]
95    InvalidDBCompactionStyle(String),
96
97    #[error("Invalid database compression type: {0}")]
98    InvalidDBCompressionType(String),
99
100    #[error("unknown database column family: {0}")]
101    UnknownColumnFamily(&'static str),
102
103    #[error(transparent)]
104    ProtobufDecode(#[from] prost::DecodeError),
105
106    #[error("Unknown key type")]
107    UnknownKeyType,
108
109    #[error(transparent)]
110    Uri(#[from] axum::http::uri::InvalidUri),
111
112    #[error("IO error: {0}")]
113    Io(#[from] std::io::Error),
114
115    #[error("TLS envelope error: {0}")]
116    TlsEnvelope(#[from] crate::tls_envelope::TlsEnvelopeError),
117
118    #[error("Certificate generation error: {0}")]
119    Certificate(#[from] rcgen::Error),
120
121    #[error("TLS error: {0}")]
122    Tls(String),
123
124    #[error("Service not found: {0}")]
125    ServiceNotFound(crate::types::ServiceId),
126}
127
128/// Generates a random challenge string to be used in the authentication process.
129///
130/// This should be sent to the client to be signed with the user's private key.
131pub fn generate_challenge<R: Rng + CryptoRng>(rng: &mut R) -> [u8; 32] {
132    let mut challenge = [0u8; 32];
133    rng.fill(&mut challenge);
134    challenge
135}
136
137/// Verifies the challenge solution sent from the client.
138pub fn verify_challenge(
139    challenge: &[u8; 32],
140    signature: &[u8],
141    pub_key: &[u8],
142    key_type: types::KeyType,
143) -> Result<bool, Error> {
144    match key_type {
145        types::KeyType::Unknown => Err(Error::UnknownKeyType),
146        types::KeyType::Ecdsa => verify_challenge_ecdsa(challenge, signature, pub_key),
147        types::KeyType::Sr25519 => verify_challenge_sr25519(challenge, signature, pub_key),
148    }
149}
150
151/// Verifies the challenge solution using ECDSA.
152fn verify_challenge_ecdsa(
153    challenge: &[u8; 32],
154    signature: &[u8],
155    pub_key: &[u8],
156) -> Result<bool, Error> {
157    use k256::ecdsa::signature::hazmat::PrehashVerifier;
158    let pub_key = k256::ecdsa::VerifyingKey::from_sec1_bytes(pub_key).map_err(Error::K256)?;
159    let signature = k256::ecdsa::Signature::try_from(signature).map_err(Error::K256)?;
160    Ok(pub_key.verify_prehash(challenge, &signature).is_ok())
161}
162
163/// Verifies the challenge solution using SR25519.
164///
165/// Note: the signing context is `tangle` and the challenge is passed as bytes, not hashed.
166fn verify_challenge_sr25519(
167    challenge: &[u8; 32],
168    signature: &[u8],
169    pub_key: &[u8],
170) -> Result<bool, Error> {
171    // We must make sure that this is the same as declared in the substrate source code.
172    const CTX: &[u8] = b"substrate";
173    let pub_key = schnorrkel::PublicKey::from_bytes(pub_key).map_err(Error::Schnorrkel)?;
174    let signature = schnorrkel::Signature::from_bytes(signature).map_err(Error::Schnorrkel)?;
175    Ok(pub_key.verify_simple(CTX, challenge, &signature).is_ok())
176}
177
178#[cfg(test)]
179mod lib_tests {
180    use super::*;
181
182    use crate::types::{KeyType, VerifyChallengeRequest};
183    use k256::ecdsa::SigningKey;
184
185    #[test]
186    fn test_generate_challenge() {
187        // Test with system RNG
188        let mut rng = blueprint_std::BlueprintRng::new();
189        let challenge1 = generate_challenge(&mut rng);
190
191        // Generate another challenge with the same RNG
192        let challenge2 = generate_challenge(&mut rng);
193
194        // Challenges should be different
195        assert_ne!(challenge1, challenge2);
196
197        // Should produce a non-zero challenge
198        assert_ne!(challenge1, [0u8; 32]);
199    }
200
201    #[test]
202    fn test_verify_challenge_ecdsa_valid() {
203        let mut rng = blueprint_std::BlueprintRng::new();
204
205        // Generate a random challenge
206        let challenge = generate_challenge(&mut rng);
207
208        // Generate a key pair
209        let signing_key = SigningKey::random(&mut rng);
210        let verification_key = signing_key.verifying_key();
211        let public_key = verification_key.to_sec1_bytes();
212
213        // Sign the challenge
214        let signature = &signing_key.sign_prehash_recoverable(&challenge).unwrap().0;
215
216        // Verify the signature
217        let result =
218            verify_challenge_ecdsa(&challenge, signature.to_bytes().as_slice(), &public_key);
219        assert!(result.is_ok());
220        assert!(result.unwrap());
221    }
222
223    #[test]
224    fn test_verify_challenge_ecdsa_invalid_signature() {
225        let mut rng = blueprint_std::BlueprintRng::new();
226
227        // Generate random challenges
228        let challenge = generate_challenge(&mut rng);
229        let different_challenge = generate_challenge(&mut rng);
230
231        // Generate a key pair
232        let signing_key = SigningKey::random(&mut rng);
233        let verification_key = signing_key.verifying_key();
234        let public_key = verification_key.to_sec1_bytes();
235
236        // Sign a different challenge
237        let signature = &signing_key
238            .sign_prehash_recoverable(&different_challenge)
239            .unwrap()
240            .0;
241        // Verification should fail because signature doesn't match the challenge
242        let result =
243            verify_challenge_ecdsa(&challenge, signature.to_bytes().as_slice(), &public_key);
244        assert!(result.is_ok());
245        assert!(!result.unwrap());
246    }
247
248    #[test]
249    fn test_verify_challenge_ecdsa_invalid_key() {
250        let mut rng = blueprint_std::BlueprintRng::new();
251
252        // Generate a random challenge
253        let challenge = generate_challenge(&mut rng);
254
255        // Generate a key pair
256        let signing_key = SigningKey::random(&mut rng);
257
258        // Generate a different key pair
259        let different_signing_key = SigningKey::random(&mut rng);
260        let different_verification_key = different_signing_key.verifying_key();
261        let different_public_key = different_verification_key.to_sec1_bytes();
262
263        // Sign with first key
264        let signature = &signing_key.sign_prehash_recoverable(&challenge).unwrap().0;
265
266        // Verify with second key - should fail
267        let result = verify_challenge_ecdsa(
268            &challenge,
269            signature.to_bytes().as_slice(),
270            &different_public_key,
271        );
272        assert!(result.is_ok());
273        assert!(!result.unwrap());
274    }
275
276    #[test]
277    fn test_verify_challenge_unknown_key_type() {
278        let mut rng = blueprint_std::BlueprintRng::new();
279        let challenge = generate_challenge(&mut rng);
280        let result = verify_challenge(&challenge, &[0u8; 64], &[0u8; 33], KeyType::Unknown);
281        assert!(matches!(result, Err(Error::UnknownKeyType)));
282    }
283
284    #[test]
285    fn test_verify_challenge_integration() {
286        let mut rng = blueprint_std::BlueprintRng::new();
287
288        // Generate a random challenge
289        let challenge = generate_challenge(&mut rng);
290
291        // Generate an ECDSA key pair
292        let signing_key = SigningKey::random(&mut rng);
293        let verification_key = signing_key.verifying_key();
294        let public_key = verification_key.to_sec1_bytes();
295
296        // Sign the challenge with ECDSA
297        let signature = &signing_key.sign_prehash_recoverable(&challenge).unwrap().0;
298
299        // Verify via the main verify_challenge function
300        let result = verify_challenge(
301            &challenge,
302            signature.to_bytes().as_slice(),
303            &public_key,
304            KeyType::Ecdsa,
305        );
306        assert!(result.is_ok());
307        assert!(result.unwrap());
308    }
309
310    #[test]
311    fn test_verify_challenge_sr25519_error_handling() {
312        let mut rng = blueprint_std::BlueprintRng::new();
313        let challenge = generate_challenge(&mut rng);
314
315        // Create invalid signature and public key data
316        let invalid_signature = [0u8; 64];
317        let invalid_pub_key = [0u8; 32];
318
319        // This should return an error since the signature and public key are invalid
320        let result = verify_challenge_sr25519(&challenge, &invalid_signature, &invalid_pub_key);
321        assert!(result.is_err());
322
323        // Verify the error is a Schnorrkel error
324        match result {
325            Err(Error::Schnorrkel(_)) => {}
326            _ => panic!("Expected Schnorrkel error"),
327        }
328    }
329
330    #[test]
331    fn js_compat_ecdsa() {
332        // Test ECDSA compatibility with JavaScript
333        // See `fixtures/sign.ts` for the original JavaScript code that generates this data.
334        let data = serde_json::json!({
335            "pub_key": "020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1",
336            "key_type": "Ecdsa",
337            "challenge": "0000000000000000000000000000000000000000000000000000000000000000",
338            "signature": "26138be19cfc76e800bdcbba5e3bbc5bd79168cd06ea6afd5be6860d23d5e0340c728508ca0b47b49627b5560fbca6cdd92cbf6ac402d0941bba7e42b9d7a20c",
339            "expires_at": 0
340        });
341
342        let req: VerifyChallengeRequest = serde_json::from_value(data).unwrap();
343        let result = verify_challenge(
344            &req.challenge,
345            &req.signature,
346            &req.challenge_request.pub_key,
347            req.challenge_request.key_type,
348        );
349        assert!(
350            result.is_ok(),
351            "Failed to verify ECDSA challenge: {}",
352            result.err().unwrap()
353        );
354        assert!(result.is_ok(), "ECDSA verification failed");
355    }
356
357    #[test]
358    fn js_compat_sr25519() {
359        // Test Sr25519 compatibility with JavaScript
360        // See `fixtures/sign_sr25519.ts` for the original JavaScript code that generates this data.
361        let data = serde_json::json!({
362            "pub_key": "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
363            "key_type": "Sr25519",
364            "challenge": "0000000000000000000000000000000000000000000000000000000000000000",
365            "signature": "f05fa2a2074d5295a34aae0d5383792a6cc34304c9cb4f6a0c577df4b374fe7bab051bd7570415578ba2da67e056d8f89b420d2e5b82412dc0f0e02877b9e48c",
366            "expires_at": 0
367        });
368
369        let req: VerifyChallengeRequest = serde_json::from_value(data).unwrap();
370        let result = verify_challenge(
371            &req.challenge,
372            &req.signature,
373            &req.challenge_request.pub_key,
374            req.challenge_request.key_type,
375        );
376        assert!(
377            result.is_ok(),
378            "Failed to verify Sr25519 challenge: {}",
379            result.err().unwrap()
380        );
381        assert!(result.is_ok(), "Sr25519 verification failed");
382    }
383}