#nonce #security #replay-attack

nonce-auth

A secure nonce-based authentication library for preventing replay attacks

1 unstable release

Uses new Rust 2024

new 0.1.0 May 25, 2025

#272 in Authentication

MIT/Apache

70KB
606 lines

Nonce Auth

CI codecov Crates.io Documentation License

A Rust-based secure nonce authentication library that provides one-time token (nonce) generation, signing, and verification functionality to effectively prevent replay attacks.

Features

  • 🔐 HMAC-SHA256 Signing - Cryptographic signing of requests using shared secrets
  • Timestamp Window Validation - Prevents replay attacks from expired requests
  • 🔑 One-time Nonce - Ensures each nonce can only be used once
  • 💾 SQLite Persistence - Automatic nonce storage and cleanup management
  • 🎯 Context Isolation - Support for nonce isolation across different business scenarios
  • 🚀 Async Support - Fully asynchronous API design
  • 🛡️ Security Protection - Constant-time comparison to prevent timing attacks
  • 📦 Client-Server Separation - Clean separation of client and server responsibilities

Architecture

Client-Server Separation Design

The library provides two independent managers for clear separation of responsibilities:

NonceClient - Client-side Manager

  • Responsible for generating signed requests
  • No database dependencies required
  • Lightweight design suitable for client-side use

NonceServer - Server-side Manager

  • Responsible for verifying signed requests
  • Manages nonce storage and cleanup
  • Includes timestamp validation and replay attack prevention

NonceManager - Unified Manager (Backward Compatible)

  • Contains both client and server functionality
  • Suitable for monolithic applications or simple scenarios

Parameter Explanation

  • default_ttl: Nonce time-to-live, representing the duration from generation to expiration, defaults to 5 minutes
  • time_window: Timestamp validation window, defaults to 1 minute

Both work together to prevent replay attacks.

Important Notes

  • The server uses local SQLite for nonce persistence, please consider using with connection sticky policies

Quick Start

Add Dependencies

[dependencies]
nonce-auth = "0.1.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Complete Client-Server Example

JavaScript Client

// client.js
import crypto from 'crypto';

class NonceClient {
    constructor(secret) {
        this.secret = secret;
    }

    createSignedRequest() {
        const timestamp = Math.floor(Date.now() / 1000);
        const nonce = crypto.randomUUID();
        const signature = this.sign(timestamp.toString(), nonce);
        
        return {
            timestamp,
            nonce,
            signature,
            secret: this.secret // Include secret for server verification
        };
    }

    sign(timestamp, nonce) {
        const hmac = crypto.createHmac('sha256', this.secret);
        hmac.update(timestamp);
        hmac.update(nonce);
        return hmac.digest('hex');
    }
}

// Usage example
async function makeAuthenticatedRequest() {
    // Generate a random secret for this session
    const secret = crypto.randomBytes(32).toString('hex');
    const client = new NonceClient(secret);
    
    const signedRequest = client.createSignedRequest();
    
    try {
        const response = await fetch('http://localhost:3000/api/protected', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                data: 'Hello, secure world!',
                auth: signedRequest
            })
        });
        
        const result = await response.json();
        console.log('Server response:', result);
    } catch (error) {
        console.error('Request failed:', error);
    }
}

makeAuthenticatedRequest();

Rust Server

// server.rs
use nonce_auth::NonceServer;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use warp::Filter;

#[derive(Deserialize)]
struct AuthenticatedRequest {
    data: String,
    auth: AuthData,
}

#[derive(Deserialize)]
struct AuthData {
    timestamp: u64,
    nonce: String,
    signature: String,
    secret: String,
}

#[derive(Serialize)]
struct ApiResponse {
    success: bool,
    message: String,
    data: Option<String>,
}

#[tokio::main]
async fn main() {
    // Initialize the nonce server database
    NonceServer::init().await.expect("Failed to initialize database");

    let protected_route = warp::path("api")
        .and(warp::path("protected"))
        .and(warp::post())
        .and(warp::body::json())
        .and_then(handle_protected_request);

    let cors = warp::cors()
        .allow_any_origin()
        .allow_headers(vec!["content-type"])
        .allow_methods(vec!["POST"]);

    println!("Server running on http://localhost:3000");
    
    warp::serve(protected_route.with(cors))
        .run(([127, 0, 0, 1], 3000))
        .await;
}

async fn handle_protected_request(
    req: AuthenticatedRequest,
) -> Result<impl warp::Reply, warp::Rejection> {
    // Create server with client's secret
    let server = NonceServer::new(
        req.auth.secret.as_bytes(),
        Some(Duration::from_secs(300)), // 5 minutes TTL
        Some(Duration::from_secs(60)),  // 1 minute time window
    );

    // Create signed request from auth data
    let signed_request = nonce_auth::SignedRequest {
        timestamp: req.auth.timestamp,
        nonce: req.auth.nonce,
        signature: req.auth.signature,
    };

    // Verify the request
    match server.verify_signed_request(&signed_request, Some("api_call")).await {
        Ok(()) => {
            let response = ApiResponse {
                success: true,
                message: "Request authenticated successfully".to_string(),
                data: Some(format!("Processed: {}", req.data)),
            };
            Ok(warp::reply::json(&response))
        }
        Err(e) => {
            let response = ApiResponse {
                success: false,
                message: format!("Authentication failed: {:?}", e),
                data: None,
            };
            Ok(warp::reply::json(&response))
        }
    }
}

Simple Usage (Client-Server Separation)

use std::time::Duration;
use nonce_auth::{NonceClient, NonceServer};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Shared secret between client and server
    let secret = b"your_secure_secret_key_here";

    // Initialize server
    NonceServer::init().await?;
    let server = NonceServer::new(
        secret,
        Some(Duration::from_secs(300)), // 5 minutes nonce TTL
        Some(Duration::from_secs(60)),  // 1 minute timestamp validation window
    );

    // Initialize client
    let client = NonceClient::new(secret);

    // Client generates signed request
    let signed_request = client.create_signed_request()?;
    println!("Generated signed request: {:?}", signed_request);
    
    // Server verifies signed request
    match server.verify_signed_request(&signed_request, None).await {
        Ok(()) => println!("✅ Request verified successfully"),
        Err(e) => println!("❌ Verification failed: {}", e),
    }

    // Try to reuse the same nonce (should fail)
    match server.verify_signed_request(&signed_request, None).await {
        Ok(()) => println!("❌ This should not happen - nonce reuse detected"),
        Err(e) => println!("✅ Correctly rejected duplicate nonce: {:?}", e),
    }

    Ok(())
}

Example Authentication Flow Sequence Diagram

sequenceDiagram
    participant JSClient as JavaScript Client
    participant RustServer as Rust Server
    participant DB as SQLite Database

    Note over JSClient, DB: Complete Authentication Flow

    JSClient->>JSClient: 1. Generate random secret
    JSClient->>JSClient: 2. Generate UUID nonce
    JSClient->>JSClient: 3. Create timestamp
    JSClient->>JSClient: 4. Sign (timestamp + nonce) with HMAC-SHA256
    
    JSClient->>RustServer: 5. POST /api/protected<br/>{data, auth: {timestamp, nonce, signature, secret}}
    
    RustServer->>RustServer: 6. Create NonceServer with client's secret
    RustServer->>RustServer: 7. Verify timestamp within window
    
    alt Timestamp out of window
        RustServer-->>JSClient: 401 Timestamp expired
    end
    
    RustServer->>RustServer: 8. Verify HMAC signature
    
    alt Invalid signature
        RustServer-->>JSClient: 401 Invalid signature
    end
    
    RustServer->>DB: 9. Check if nonce exists in context
    
    alt Nonce already used
        RustServer-->>JSClient: 401 Duplicate nonce
    end
    
    RustServer->>DB: 10. Store nonce with context
    RustServer->>RustServer: 11. Process business logic
    RustServer-->>JSClient: 200 Success response
    
    Note over RustServer, DB: Background cleanup
    RustServer->>DB: Cleanup expired nonces as needed

API Documentation

NonceClient

Constructor

pub fn new(secret: &[u8]) -> Self
  • secret: Secret key used for signing

Methods

Create Signed Request
pub fn create_signed_request(&self) -> Result<SignedRequest, NonceError>

Generates a complete request containing timestamp, nonce, and signature.

Sign
pub fn sign(&self, timestamp: &str, nonce: &str) -> Result<String, NonceError>

Generates HMAC-SHA256 signature for given timestamp and nonce.

NonceServer

Constructor

pub fn new(
    secret: &[u8], 
    default_ttl: Option<Duration>, 
    time_window: Option<Duration>
) -> Self
  • secret: Secret key used for verification
  • default_ttl: Default nonce expiration time (default: 5 minutes)
  • time_window: Allowed time window for timestamp validation (default: 1 minute)

Methods

Verify Signed Request
pub async fn verify_signed_request(
    &self, 
    request: &SignedRequest, 
    context: Option<&str>
) -> Result<(), NonceError>

Verifies request integrity including time window, nonce validity, and signature correctness.

Initialize Database
pub async fn init() -> Result<(), NonceError>

Creates necessary database tables and indexes.

SignedRequest

pub struct SignedRequest {
    pub timestamp: u64,    // Unix timestamp
    pub nonce: String,     // UUID format one-time token
    pub signature: String, // HMAC-SHA256 signature
}

Error Types

pub enum NonceError {
    DuplicateNonce,         // Nonce already used
    ExpiredNonce,           // Nonce expired
    InvalidSignature,       // Invalid signature
    TimestampOutOfWindow,   // Timestamp outside allowed window
    DatabaseError(String),  // Database error
    CryptoError(String),    // Cryptographic error
}

Typical Use Cases

1. API Authentication

  • Client first obtains authentication token
  • Uses token to access protected APIs
  • Each token can only be used once

2. Form Submission Protection

  • Generate nonce when rendering form
  • Verify nonce when submitting
  • Prevents duplicate form submissions

3. Microservice Authentication

  • Service A generates nonce for requests
  • Service B verifies requests from Service A
  • Ensures request uniqueness and authenticity

4. Session-based Authentication

  • Client generates random secret per session
  • Server verifies requests using client's secret
  • Provides stateless authentication

Security Features

Replay Attack Prevention

  1. Time Window Limitation: Only accepts requests within specified time window
  2. One-time Nonce: Each nonce is deleted after verification, ensuring no reuse
  3. Context Isolation: Nonces from different business scenarios are isolated

Timing Attack Prevention

  • Uses constant-time comparison algorithms for signature verification

Cryptographic Strength

  • Uses HMAC-SHA256 algorithm to ensure signature integrity and authenticity
  • Supports custom secret key lengths

Performance Optimization

  • Automatic background cleanup of expired nonce records
  • Database index optimization for query performance
  • Asynchronous design supports high-concurrency scenarios

Dependencies

  • hmac - HMAC signing
  • sha2 - SHA256 hashing
  • turbosql - SQLite ORM
  • uuid - UUID generation
  • serde - Serialization support
  • tokio - Async runtime
  • thiserror - Error handling

License

MIT OR Apache-2.0

Contributing

Issues and Pull Requests are welcome!

Dependencies

~28–38MB
~608K SLoC