Skip to main content

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
88    /// Error related to the SR25519 signature verification.
89    #[error("Schnorrkel error: {0}")]
90    Schnorrkel(schnorrkel::SignatureError),
91
92    /// Error related to the BN254 BLS signature verification.
93    #[error("BN254 BLS error: {0}")]
94    Bn254Bls(String),
95
96    #[error(transparent)]
97    RocksDB(#[from] rocksdb::Error),
98
99    #[error("Invalid database compaction style: {0}")]
100    InvalidDBCompactionStyle(String),
101
102    #[error("Invalid database compression type: {0}")]
103    InvalidDBCompressionType(String),
104
105    #[error("unknown database column family: {0}")]
106    UnknownColumnFamily(&'static str),
107
108    #[error(transparent)]
109    ProtobufDecode(#[from] prost::DecodeError),
110
111    #[error("Unknown key type")]
112    UnknownKeyType,
113
114    #[error(transparent)]
115    Uri(#[from] axum::http::uri::InvalidUri),
116
117    #[error("IO error: {0}")]
118    Io(#[from] std::io::Error),
119
120    #[error("TLS envelope error: {0}")]
121    TlsEnvelope(#[from] crate::tls_envelope::TlsEnvelopeError),
122
123    #[error("Certificate generation error: {0}")]
124    Certificate(#[from] rcgen::Error),
125
126    #[error("TLS error: {0}")]
127    Tls(String),
128
129    #[error("Service not found: {0}")]
130    ServiceNotFound(crate::types::ServiceId),
131}
132
133/// Generates a random challenge string to be used in the authentication process.
134///
135/// This should be sent to the client to be signed with the user's private key.
136pub fn generate_challenge<R: Rng + CryptoRng>(rng: &mut R) -> [u8; 32] {
137    let mut challenge = [0u8; 32];
138    rng.fill(&mut challenge);
139    challenge
140}
141
142/// Verifies the challenge solution sent from the client.
143pub fn verify_challenge(
144    challenge: &[u8; 32],
145    signature: &[u8],
146    pub_key: &[u8],
147    key_type: types::KeyType,
148) -> Result<bool, Error> {
149    match key_type {
150        types::KeyType::Unknown => Err(Error::UnknownKeyType),
151        types::KeyType::Ecdsa => verify_challenge_ecdsa(challenge, signature, pub_key),
152        types::KeyType::Sr25519 => verify_challenge_sr25519(challenge, signature, pub_key),
153        types::KeyType::Bn254Bls => verify_challenge_bn254_bls(challenge, signature, pub_key),
154    }
155}
156
157/// Verifies the challenge solution using ECDSA.
158fn verify_challenge_ecdsa(
159    challenge: &[u8; 32],
160    signature: &[u8],
161    pub_key: &[u8],
162) -> Result<bool, Error> {
163    use k256::ecdsa::signature::hazmat::PrehashVerifier;
164    let pub_key = k256::ecdsa::VerifyingKey::from_sec1_bytes(pub_key).map_err(Error::K256)?;
165    let signature = k256::ecdsa::Signature::try_from(signature).map_err(Error::K256)?;
166    Ok(pub_key.verify_prehash(challenge, &signature).is_ok())
167}
168
169/// Verifies the challenge solution using SR25519.
170///
171/// Note: the signing context is `substrate` and the challenge is passed as bytes, not hashed.
172fn verify_challenge_sr25519(
173    challenge: &[u8; 32],
174    signature: &[u8],
175    pub_key: &[u8],
176) -> Result<bool, Error> {
177    const CTX: &[u8] = b"substrate";
178    let pub_key = schnorrkel::PublicKey::from_bytes(pub_key).map_err(Error::Schnorrkel)?;
179    let signature = schnorrkel::Signature::from_bytes(signature).map_err(Error::Schnorrkel)?;
180    Ok(pub_key.verify_simple(CTX, challenge, &signature).is_ok())
181}
182
183/// Verifies the challenge solution using BN254 BLS.
184///
185/// The public key is expected to be a compressed G2Affine point.
186/// The signature is expected to be a compressed G1Affine point.
187fn verify_challenge_bn254_bls(
188    challenge: &[u8; 32],
189    signature: &[u8],
190    pub_key: &[u8],
191) -> Result<bool, Error> {
192    use blueprint_crypto::BytesEncoding;
193    use blueprint_crypto::bn254::{ArkBlsBn254Public, ArkBlsBn254Signature};
194
195    let public_key = ArkBlsBn254Public::from_bytes(pub_key)
196        .map_err(|e| Error::Bn254Bls(format!("Invalid public key: {e:?}")))?;
197    let sig = ArkBlsBn254Signature::from_bytes(signature)
198        .map_err(|e| Error::Bn254Bls(format!("Invalid signature: {e:?}")))?;
199
200    Ok(blueprint_crypto::bn254::verify(
201        public_key.0,
202        challenge,
203        sig.0,
204    ))
205}
206
207#[cfg(test)]
208mod lib_tests {
209    use super::*;
210
211    use crate::types::{KeyType, VerifyChallengeRequest};
212    use k256::ecdsa::SigningKey;
213
214    #[test]
215    fn test_generate_challenge() {
216        // Test with system RNG
217        let mut rng = blueprint_std::BlueprintRng::new();
218        let challenge1 = generate_challenge(&mut rng);
219
220        // Generate another challenge with the same RNG
221        let challenge2 = generate_challenge(&mut rng);
222
223        // Challenges should be different
224        assert_ne!(challenge1, challenge2);
225
226        // Should produce a non-zero challenge
227        assert_ne!(challenge1, [0u8; 32]);
228    }
229
230    #[test]
231    fn test_verify_challenge_ecdsa_valid() {
232        let mut rng = blueprint_std::BlueprintRng::new();
233
234        // Generate a random challenge
235        let challenge = generate_challenge(&mut rng);
236
237        // Generate a key pair
238        let signing_key = SigningKey::random(&mut rng);
239        let verification_key = signing_key.verifying_key();
240        let public_key = verification_key.to_sec1_bytes();
241
242        // Sign the challenge
243        let signature = &signing_key.sign_prehash_recoverable(&challenge).unwrap().0;
244
245        // Verify the signature
246        let result =
247            verify_challenge_ecdsa(&challenge, signature.to_bytes().as_slice(), &public_key);
248        assert!(result.is_ok());
249        assert!(result.unwrap());
250    }
251
252    #[test]
253    fn test_verify_challenge_ecdsa_invalid_signature() {
254        let mut rng = blueprint_std::BlueprintRng::new();
255
256        // Generate random challenges
257        let challenge = generate_challenge(&mut rng);
258        let different_challenge = generate_challenge(&mut rng);
259
260        // Generate a key pair
261        let signing_key = SigningKey::random(&mut rng);
262        let verification_key = signing_key.verifying_key();
263        let public_key = verification_key.to_sec1_bytes();
264
265        // Sign a different challenge
266        let signature = &signing_key
267            .sign_prehash_recoverable(&different_challenge)
268            .unwrap()
269            .0;
270        // Verification should fail because signature doesn't match the challenge
271        let result =
272            verify_challenge_ecdsa(&challenge, signature.to_bytes().as_slice(), &public_key);
273        assert!(result.is_ok());
274        assert!(!result.unwrap());
275    }
276
277    #[test]
278    fn test_verify_challenge_ecdsa_invalid_key() {
279        let mut rng = blueprint_std::BlueprintRng::new();
280
281        // Generate a random challenge
282        let challenge = generate_challenge(&mut rng);
283
284        // Generate a key pair
285        let signing_key = SigningKey::random(&mut rng);
286
287        // Generate a different key pair
288        let different_signing_key = SigningKey::random(&mut rng);
289        let different_verification_key = different_signing_key.verifying_key();
290        let different_public_key = different_verification_key.to_sec1_bytes();
291
292        // Sign with first key
293        let signature = &signing_key.sign_prehash_recoverable(&challenge).unwrap().0;
294
295        // Verify with second key - should fail
296        let result = verify_challenge_ecdsa(
297            &challenge,
298            signature.to_bytes().as_slice(),
299            &different_public_key,
300        );
301        assert!(result.is_ok());
302        assert!(!result.unwrap());
303    }
304
305    #[test]
306    fn test_verify_challenge_unknown_key_type() {
307        let mut rng = blueprint_std::BlueprintRng::new();
308        let challenge = generate_challenge(&mut rng);
309        let result = verify_challenge(&challenge, &[0u8; 64], &[0u8; 33], KeyType::Unknown);
310        assert!(matches!(result, Err(Error::UnknownKeyType)));
311    }
312
313    #[test]
314    fn test_verify_challenge_integration() {
315        let mut rng = blueprint_std::BlueprintRng::new();
316
317        // Generate a random challenge
318        let challenge = generate_challenge(&mut rng);
319
320        // Generate an ECDSA key pair
321        let signing_key = SigningKey::random(&mut rng);
322        let verification_key = signing_key.verifying_key();
323        let public_key = verification_key.to_sec1_bytes();
324
325        // Sign the challenge with ECDSA
326        let signature = &signing_key.sign_prehash_recoverable(&challenge).unwrap().0;
327
328        // Verify via the main verify_challenge function
329        let result = verify_challenge(
330            &challenge,
331            signature.to_bytes().as_slice(),
332            &public_key,
333            KeyType::Ecdsa,
334        );
335        assert!(result.is_ok());
336        assert!(result.unwrap());
337    }
338
339    #[test]
340    fn test_verify_challenge_sr25519_error_handling() {
341        let mut rng = blueprint_std::BlueprintRng::new();
342        let challenge = generate_challenge(&mut rng);
343
344        // Create invalid signature and public key data
345        let invalid_signature = [0u8; 64];
346        let invalid_pub_key = [0u8; 32];
347
348        // This should return an error since the signature and public key are invalid
349        let result = verify_challenge_sr25519(&challenge, &invalid_signature, &invalid_pub_key);
350        assert!(result.is_err());
351
352        // Verify the error is a Schnorrkel error
353        match result {
354            Err(Error::Schnorrkel(_)) => {}
355            _ => panic!("Expected Schnorrkel error"),
356        }
357    }
358
359    #[test]
360    fn js_compat_ecdsa() {
361        // Test ECDSA compatibility with JavaScript
362        // See `fixtures/sign.ts` for the original JavaScript code that generates this data.
363        let data = serde_json::json!({
364            "pub_key": "020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1",
365            "key_type": "Ecdsa",
366            "challenge": "0000000000000000000000000000000000000000000000000000000000000000",
367            "signature": "26138be19cfc76e800bdcbba5e3bbc5bd79168cd06ea6afd5be6860d23d5e0340c728508ca0b47b49627b5560fbca6cdd92cbf6ac402d0941bba7e42b9d7a20c",
368            "expires_at": 0
369        });
370
371        let req: VerifyChallengeRequest = serde_json::from_value(data).unwrap();
372        let result = verify_challenge(
373            &req.challenge,
374            &req.signature,
375            &req.challenge_request.pub_key,
376            req.challenge_request.key_type,
377        );
378        assert!(
379            result.is_ok(),
380            "Failed to verify ECDSA challenge: {}",
381            result.err().unwrap()
382        );
383        assert!(result.is_ok(), "ECDSA verification failed");
384    }
385
386    #[test]
387    fn js_compat_sr25519() {
388        // Test Sr25519 compatibility with JavaScript
389        // See `fixtures/sign_sr25519.ts` for the original JavaScript code that generates this data.
390        let data = serde_json::json!({
391            "pub_key": "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
392            "key_type": "Sr25519",
393            "challenge": "0000000000000000000000000000000000000000000000000000000000000000",
394            "signature": "f05fa2a2074d5295a34aae0d5383792a6cc34304c9cb4f6a0c577df4b374fe7bab051bd7570415578ba2da67e056d8f89b420d2e5b82412dc0f0e02877b9e48c",
395            "expires_at": 0
396        });
397
398        let req: VerifyChallengeRequest = serde_json::from_value(data).unwrap();
399        let result = verify_challenge(
400            &req.challenge,
401            &req.signature,
402            &req.challenge_request.pub_key,
403            req.challenge_request.key_type,
404        );
405        assert!(
406            result.is_ok(),
407            "Failed to verify Sr25519 challenge: {}",
408            result.err().unwrap()
409        );
410        assert!(result.is_ok(), "Sr25519 verification failed");
411    }
412}