blueprint-auth 0.2.0-alpha.1

Blueprint HTTP/WS Authentication
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
//! Authentication module for the Blueprint SDK.
//!
//! This module provides a three-tier token authentication system:
//!
//! 1. **API Keys** (`ak_xxxxx.yyyyy`) - Long-lived credentials for service authentication
//! 2. **Access Tokens** (`v4.local.xxxxx`) - Short-lived Paseto tokens for authorization
//! 3. **Legacy Tokens** (`id|token`) - Deprecated format for backward compatibility
//!
//! # Architecture
//!
//! The authentication flow follows these steps:
//! 1. Client authenticates with API key
//! 2. API key is exchanged for a short-lived access token
//! 3. Access token is used for subsequent requests
//! 4. Token refresh happens automatically before expiration
//!
//! # Security Features
//!
//! - Cryptographic tenant binding prevents impersonation
//! - Header re-validation prevents injection attacks  
//! - Persistent key storage with secure permissions
//! - Automatic token rotation and refresh
//!
//! # Example
//!
//! ```no_run
//! use blueprint_auth::proxy::AuthenticatedProxy;
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     // Initialize proxy with persistent storage
//!     let proxy = AuthenticatedProxy::new("/var/lib/auth/db")?;
//!
//!     // Start the proxy server
//!     let router = proxy.router();
//!     Ok(())
//! }
//! ```

use blueprint_std::rand::{CryptoRng, Rng};

/// Long-lived API key management
pub mod api_keys;
/// Generates API Tokens for the authentication process.
pub mod api_tokens;
/// Unified authentication token types
pub mod auth_token;
/// Certificate Authority utilities for mTLS
pub mod certificate_authority;
/// The database module for the authentication process.
pub mod db;
/// Database models
pub mod models;
/// OAuth 2.0 JWT assertion verifier and per-service policy
pub mod oauth;
/// Paseto token generation and validation
pub mod paseto_tokens;
/// Authenticated Proxy Server built on top of Axum.
pub mod proxy;
/// Request-level auth context parsing and extractors
pub mod request_auth;
/// Request extension plumbing for client certificate identity
pub mod request_extensions;
/// TLS assets management
pub mod tls_assets;
/// TLS client management for outbound connections
pub mod tls_client;
/// TLS envelope encryption for certificate material
pub mod tls_envelope;
/// TLS listener for dual socket support (HTTP + HTTPS)
pub mod tls_listener;
/// Holds the authentication-related types.
pub mod types;
/// Header validation utilities
pub mod validation;

#[cfg(test)]
mod test_client;

#[cfg(test)]
mod tests;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// Error related to the ECDSA signature verification.
    #[error("k256 error: {0}")]
    K256(k256::ecdsa::Error),

    /// Error related to the SR25519 signature verification.
    #[error("Schnorrkel error: {0}")]
    Schnorrkel(schnorrkel::SignatureError),

    /// Error related to the BN254 BLS signature verification.
    #[error("BN254 BLS error: {0}")]
    Bn254Bls(String),

    #[error(transparent)]
    RocksDB(#[from] rocksdb::Error),

    #[error("Invalid database compaction style: {0}")]
    InvalidDBCompactionStyle(String),

    #[error("Invalid database compression type: {0}")]
    InvalidDBCompressionType(String),

    #[error("unknown database column family: {0}")]
    UnknownColumnFamily(&'static str),

    #[error(transparent)]
    ProtobufDecode(#[from] prost::DecodeError),

    #[error("Unknown key type")]
    UnknownKeyType,

    #[error(transparent)]
    Uri(#[from] axum::http::uri::InvalidUri),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("TLS envelope error: {0}")]
    TlsEnvelope(#[from] crate::tls_envelope::TlsEnvelopeError),

    #[error("Certificate generation error: {0}")]
    Certificate(#[from] rcgen::Error),

    #[error("TLS error: {0}")]
    Tls(String),

    #[error("Service not found: {0}")]
    ServiceNotFound(crate::types::ServiceId),
}

/// Generates a random challenge string to be used in the authentication process.
///
/// This should be sent to the client to be signed with the user's private key.
pub fn generate_challenge<R: Rng + CryptoRng>(rng: &mut R) -> [u8; 32] {
    let mut challenge = [0u8; 32];
    rng.fill(&mut challenge);
    challenge
}

/// Verifies the challenge solution sent from the client.
pub fn verify_challenge(
    challenge: &[u8; 32],
    signature: &[u8],
    pub_key: &[u8],
    key_type: types::KeyType,
) -> Result<bool, Error> {
    match key_type {
        types::KeyType::Unknown => Err(Error::UnknownKeyType),
        types::KeyType::Ecdsa => verify_challenge_ecdsa(challenge, signature, pub_key),
        types::KeyType::Sr25519 => verify_challenge_sr25519(challenge, signature, pub_key),
        types::KeyType::Bn254Bls => verify_challenge_bn254_bls(challenge, signature, pub_key),
    }
}

/// Verifies the challenge solution using ECDSA.
fn verify_challenge_ecdsa(
    challenge: &[u8; 32],
    signature: &[u8],
    pub_key: &[u8],
) -> Result<bool, Error> {
    use k256::ecdsa::signature::hazmat::PrehashVerifier;
    let pub_key = k256::ecdsa::VerifyingKey::from_sec1_bytes(pub_key).map_err(Error::K256)?;
    let signature = k256::ecdsa::Signature::try_from(signature).map_err(Error::K256)?;
    Ok(pub_key.verify_prehash(challenge, &signature).is_ok())
}

/// Verifies the challenge solution using SR25519.
///
/// Note: the signing context is `substrate` and the challenge is passed as bytes, not hashed.
fn verify_challenge_sr25519(
    challenge: &[u8; 32],
    signature: &[u8],
    pub_key: &[u8],
) -> Result<bool, Error> {
    const CTX: &[u8] = b"substrate";
    let pub_key = schnorrkel::PublicKey::from_bytes(pub_key).map_err(Error::Schnorrkel)?;
    let signature = schnorrkel::Signature::from_bytes(signature).map_err(Error::Schnorrkel)?;
    Ok(pub_key.verify_simple(CTX, challenge, &signature).is_ok())
}

/// Verifies the challenge solution using BN254 BLS.
///
/// The public key is expected to be a compressed G2Affine point.
/// The signature is expected to be a compressed G1Affine point.
fn verify_challenge_bn254_bls(
    challenge: &[u8; 32],
    signature: &[u8],
    pub_key: &[u8],
) -> Result<bool, Error> {
    use blueprint_crypto::BytesEncoding;
    use blueprint_crypto::bn254::{ArkBlsBn254Public, ArkBlsBn254Signature};

    let public_key = ArkBlsBn254Public::from_bytes(pub_key)
        .map_err(|e| Error::Bn254Bls(format!("Invalid public key: {e:?}")))?;
    let sig = ArkBlsBn254Signature::from_bytes(signature)
        .map_err(|e| Error::Bn254Bls(format!("Invalid signature: {e:?}")))?;

    Ok(blueprint_crypto::bn254::verify(
        public_key.0,
        challenge,
        sig.0,
    ))
}

#[cfg(test)]
mod lib_tests {
    use super::*;

    use crate::types::{KeyType, VerifyChallengeRequest};
    use k256::ecdsa::SigningKey;

    #[test]
    fn test_generate_challenge() {
        // Test with system RNG
        let mut rng = blueprint_std::BlueprintRng::new();
        let challenge1 = generate_challenge(&mut rng);

        // Generate another challenge with the same RNG
        let challenge2 = generate_challenge(&mut rng);

        // Challenges should be different
        assert_ne!(challenge1, challenge2);

        // Should produce a non-zero challenge
        assert_ne!(challenge1, [0u8; 32]);
    }

    #[test]
    fn test_verify_challenge_ecdsa_valid() {
        let mut rng = blueprint_std::BlueprintRng::new();

        // Generate a random challenge
        let challenge = generate_challenge(&mut rng);

        // Generate a key pair
        let signing_key = SigningKey::random(&mut rng);
        let verification_key = signing_key.verifying_key();
        let public_key = verification_key.to_sec1_bytes();

        // Sign the challenge
        let signature = &signing_key.sign_prehash_recoverable(&challenge).unwrap().0;

        // Verify the signature
        let result =
            verify_challenge_ecdsa(&challenge, signature.to_bytes().as_slice(), &public_key);
        assert!(result.is_ok());
        assert!(result.unwrap());
    }

    #[test]
    fn test_verify_challenge_ecdsa_invalid_signature() {
        let mut rng = blueprint_std::BlueprintRng::new();

        // Generate random challenges
        let challenge = generate_challenge(&mut rng);
        let different_challenge = generate_challenge(&mut rng);

        // Generate a key pair
        let signing_key = SigningKey::random(&mut rng);
        let verification_key = signing_key.verifying_key();
        let public_key = verification_key.to_sec1_bytes();

        // Sign a different challenge
        let signature = &signing_key
            .sign_prehash_recoverable(&different_challenge)
            .unwrap()
            .0;
        // Verification should fail because signature doesn't match the challenge
        let result =
            verify_challenge_ecdsa(&challenge, signature.to_bytes().as_slice(), &public_key);
        assert!(result.is_ok());
        assert!(!result.unwrap());
    }

    #[test]
    fn test_verify_challenge_ecdsa_invalid_key() {
        let mut rng = blueprint_std::BlueprintRng::new();

        // Generate a random challenge
        let challenge = generate_challenge(&mut rng);

        // Generate a key pair
        let signing_key = SigningKey::random(&mut rng);

        // Generate a different key pair
        let different_signing_key = SigningKey::random(&mut rng);
        let different_verification_key = different_signing_key.verifying_key();
        let different_public_key = different_verification_key.to_sec1_bytes();

        // Sign with first key
        let signature = &signing_key.sign_prehash_recoverable(&challenge).unwrap().0;

        // Verify with second key - should fail
        let result = verify_challenge_ecdsa(
            &challenge,
            signature.to_bytes().as_slice(),
            &different_public_key,
        );
        assert!(result.is_ok());
        assert!(!result.unwrap());
    }

    #[test]
    fn test_verify_challenge_unknown_key_type() {
        let mut rng = blueprint_std::BlueprintRng::new();
        let challenge = generate_challenge(&mut rng);
        let result = verify_challenge(&challenge, &[0u8; 64], &[0u8; 33], KeyType::Unknown);
        assert!(matches!(result, Err(Error::UnknownKeyType)));
    }

    #[test]
    fn test_verify_challenge_integration() {
        let mut rng = blueprint_std::BlueprintRng::new();

        // Generate a random challenge
        let challenge = generate_challenge(&mut rng);

        // Generate an ECDSA key pair
        let signing_key = SigningKey::random(&mut rng);
        let verification_key = signing_key.verifying_key();
        let public_key = verification_key.to_sec1_bytes();

        // Sign the challenge with ECDSA
        let signature = &signing_key.sign_prehash_recoverable(&challenge).unwrap().0;

        // Verify via the main verify_challenge function
        let result = verify_challenge(
            &challenge,
            signature.to_bytes().as_slice(),
            &public_key,
            KeyType::Ecdsa,
        );
        assert!(result.is_ok());
        assert!(result.unwrap());
    }

    #[test]
    fn test_verify_challenge_sr25519_error_handling() {
        let mut rng = blueprint_std::BlueprintRng::new();
        let challenge = generate_challenge(&mut rng);

        // Create invalid signature and public key data
        let invalid_signature = [0u8; 64];
        let invalid_pub_key = [0u8; 32];

        // This should return an error since the signature and public key are invalid
        let result = verify_challenge_sr25519(&challenge, &invalid_signature, &invalid_pub_key);
        assert!(result.is_err());

        // Verify the error is a Schnorrkel error
        match result {
            Err(Error::Schnorrkel(_)) => {}
            _ => panic!("Expected Schnorrkel error"),
        }
    }

    #[test]
    fn js_compat_ecdsa() {
        // Test ECDSA compatibility with JavaScript
        // See `fixtures/sign.ts` for the original JavaScript code that generates this data.
        let data = serde_json::json!({
            "pub_key": "020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1",
            "key_type": "Ecdsa",
            "challenge": "0000000000000000000000000000000000000000000000000000000000000000",
            "signature": "26138be19cfc76e800bdcbba5e3bbc5bd79168cd06ea6afd5be6860d23d5e0340c728508ca0b47b49627b5560fbca6cdd92cbf6ac402d0941bba7e42b9d7a20c",
            "expires_at": 0
        });

        let req: VerifyChallengeRequest = serde_json::from_value(data).unwrap();
        let result = verify_challenge(
            &req.challenge,
            &req.signature,
            &req.challenge_request.pub_key,
            req.challenge_request.key_type,
        );
        assert!(
            result.is_ok(),
            "Failed to verify ECDSA challenge: {}",
            result.err().unwrap()
        );
        assert!(result.is_ok(), "ECDSA verification failed");
    }

    #[test]
    fn js_compat_sr25519() {
        // Test Sr25519 compatibility with JavaScript
        // See `fixtures/sign_sr25519.ts` for the original JavaScript code that generates this data.
        let data = serde_json::json!({
            "pub_key": "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
            "key_type": "Sr25519",
            "challenge": "0000000000000000000000000000000000000000000000000000000000000000",
            "signature": "f05fa2a2074d5295a34aae0d5383792a6cc34304c9cb4f6a0c577df4b374fe7bab051bd7570415578ba2da67e056d8f89b420d2e5b82412dc0f0e02877b9e48c",
            "expires_at": 0
        });

        let req: VerifyChallengeRequest = serde_json::from_value(data).unwrap();
        let result = verify_challenge(
            &req.challenge,
            &req.signature,
            &req.challenge_request.pub_key,
            req.challenge_request.key_type,
        );
        assert!(
            result.is_ok(),
            "Failed to verify Sr25519 challenge: {}",
            result.err().unwrap()
        );
        assert!(result.is_ok(), "Sr25519 verification failed");
    }
}