rusty_paseto

repository·main·Indexed 40 days ago

https://github.com/rrrodzilla/rusty_paseto

A type-driven, ergonomic Rust implementation of the PASETO (Platform-Agnostic Security Tokens) protocol for creating and verifying secure stateless tokens. Version 0.10.1 provides a three-layer architecture (batteries_included, generic, and core) to balance functionality and binary size, supporting various PASETO versions and purposes including V4 local and public tokens. It includes support for PASERK key serialization, implicit assertions, and custom claim validation.

Tokens
47.8K
Snippets
132
Records
174
Agent score
47%

What's inside rusty_paseto

  1. Overview of PASETO Key Types by Version

    main

    PASETO versions and purposes (Local vs. Public) determine the required key types and lengths. Use the following mapping to select the correct key for your implementation:

    VersionLocal (Symmetric)Public (Asymmetric)
    V432-byte keyEd25519 keypair
    V332-byte keyP-384 ECDSA keypair
    V232-byte keyEd25519 keypair
    V132-byte keyRSA keypair
  2. Use Footers for public metadata

    main

    Footers are unencrypted, authenticated (tamper-proof) data appended to a PASETO token. They are visible without decryption and are ideal for storing metadata like Key IDs (kid) to facilitate key rotation.

    When using footers, you can extract the footer from a token string using Footer::try_from_token before performing full decryption. This allows you to inspect the metadata and select the appropriate key for parsing.

    use rusty_paseto::prelude::*;
    
    let token = PasetoBuilder::<V4, Local>::default()
        .set_claim(SubjectClaim::from("user_123"))
        .set_footer(Footer::from(r#"{\"kid\":\"key-2024-01\"}"#))
        .build(&key)?;
  3. Understand the rusty_paseto three-layer architecture

    main

    rusty_paseto is organized into three distinct layers of abstraction. You should choose a layer based on the level of control and ease of use required for your application:

    1. Prelude Layer: The highest-level, fluent API. It uses PasetoBuilder and PasetoParser and is the recommended starting point for most applications because it provides automatic claim validation and a fluent builder pattern.
    2. Generic Layer: Provides claim types and validation utilities without the builder abstraction. Use this if you need to work with claims directly or need more control over token construction without the full builder overhead.
    3. Core Layer: The lowest-level layer providing direct access to cryptographic operations. Use this for maximum control, manual nonce handling, or when building your own wrapper libraries. It does not provide automatic claim validation.

    Layer Dependencies:

    • Prelude $\rightarrow$ Generic $\rightarrow$ Core
    • Enabling the prelude feature automatically enables generic and core.
  4. Create reusable generic claim structures

    main

    To avoid duplicating common fields (like sub, aud, iss, exp), you can use a generic struct with #[serde(flatten)]. This allows you to define a base set of claims and inject application-specific custom claims into the same object.

    use serde::Deserialize;
    
    // Base claims present in all tokens
    #[derive(Deserialize)]
    struct BaseClaims<T> {
        sub: String,
        aud: String,
        iss: String,
        exp: String,
        #[serde(flatten)]
        custom: T,
    }
    
    // Application-specific custom claims
    #[derive(Deserialize)]
    struct AppCustomClaims {
        role: String,
        tenant_id: String,
    }
    
    // Usage
    let claims: BaseClaims<AppCustomClaims> = PasetoParser::<V4, Local>::default()
        .parse_into(&token, &key)?;
    
    println!("Tenant: {}", claims.custom.tenant_id);
  5. Understand the rusty_paseto architecture

    main

    The rusty_paseto crate is organized into three hierarchical architectural layers. You can choose a layer based on your need for features versus dependency weight:

    1. batteries_included: The outermost layer. It includes JWT-style claims (like exp, iat, nbf) and built-in business rules (e.g., default 1-hour expiration). This is recommended for most users.
    2. generic: An intermediate layer for advanced users. It provides a generic builder and parser with claims that you can extend or customize (e.g., using different time crates or custom default business rules).
    3. core: The most basic layer. It provides only the essential PASETO cryptographic functions (encrypt/sign and decrypt/verify) for a payload, optional footer, and optional implicit assertion. It does not include claims, business rules, or serde dependencies, making it extremely lightweight.

    All layers use a common crypto core that varies depending on the PASETO version and purpose you select via feature gates.

  6. Understand the three architectural layers of rusty_paseto

    main

    The crate is organized into three layers. You should choose the layer that best fits your needs to balance functionality against binary size and dependency weight.

    1. batteries_included (High-level)

    This is the most common layer. It includes JWT-style claims and business rules (e.g., customizable expiration, issued_at, not-before times).

    • Usage: Use this for standard application needs where you want built-in claim handling.
    • Setup: Requires specifying a version and purpose (e.g., features = ["batteries_included", "v4_local"]).
    • Import: use rusty_paseto::prelude::*;

    2. generic (Intermediate)

    Allows you to create custom versions of the batteries_included layer. It provides a generic builder and parser along with claims you can extend.

    • Usage: Use this if you need custom default claims, a different time crate, or unique business rules. Parsed tokens are returned as serde_json::Value.
    • Setup: Requires specifying a version and purpose (e.g., features = ["generic", "v4_local"]).
    • Import: use rusty_paseto::generic::*;

    3. core (Low-level)

    The most basic implementation. It handles raw PASETO crypto (Payload, optional Footer, and optional Implicit Assertion for V3/V4) using the appropriate key.

    • Usage: Use this for extremely lightweight applications that only need encryption/signing and decryption/verification without JWT-esque claims or Serde dependencies.
    • Setup: Requires specifying a version and purpose (e.g., features = ["core", "v4_local"]).
  7. Use Implicit Assertions for context binding

    main

    Implicit assertions are data cryptographically bound to the token but are not included in the token string itself. They must be provided by the caller during both token creation and token parsing.

    If the assertion provided during parsing does not match the one used during creation, parsing will fail with Error::CryptoError. This is useful for multi-tenant systems (binding a token to a tenant ID) or request binding (binding to a specific API endpoint) without exposing the binding data in the token string.

    use rusty_paseto::prelude::*;
    
    // Setting an implicit assertion during creation
    let token = PasetoBuilder::<V4, Local>::default()
        .set_claim(SubjectClaim::from("user_123"))
        .set_implicit_assertion(ImplicitAssertion::from("tenant:acme-corp"))
        .build(&key)?;
    
    // Parsing with the same implicit assertion
    let payload = PasetoParser::<V4, Local>::default()
        .set_implicit_assertion(ImplicitAssertion::from("tenant:acme-corp"))
        .parse(&token, &key)?;
  8. Understand feature dependencies in rusty_paseto

    main

    Features in rusty_paseto follow a hierarchical dependency chain. Enabling a high-level feature automatically enables its required lower-level dependencies:

    • prelude $\rightarrow$ enables generic $\rightarrow$ enables core
    • generic $\rightarrow$ enables core
    • Any version feature (e.g., v4_local) $\rightarrow$ enables core

    For example, configuring features = ["v4_local", "prelude"] will actually enable v4_local, prelude, generic, and core.

  9. Automatic time-based claim validation

    main

    The PasetoParser automatically validates standard time-based claims during the .parse() process. If these checks fail, the parser returns a specific error:

    • exp (Expiration): If the token's expiration time is in the past, it returns Error::Expired.
    • nbf (Not Before): If the token is used before its not-before time, it returns Error::NotYetValid.
    use rusty_paseto::prelude::*;
    
    // Automatic validation happens during parse
    let payload = PasetoParser::<V4, Local>::default()
        .parse(&token, &key)?;
    // If exp is in the past -> Error::Expired
    // If nbf is in the future -> Error::NotYetValid
  10. Access custom claims in the payload

    main

    After parsing a token, you can access custom claims in two ways:

    1. Manual JSON Access: Parse the resulting payload string into a serde_json::Value and access keys using JSON indexing.
    2. Typed Parsing: Use .parse_into::<T>() to deserialize the payload directly into a custom Rust struct that implements serde::Deserialize.
    // Method 1: Manual JSON Access
    let payload = PasetoParser::<V4, Local>::default().parse(&token, &key)?;
    let claims: serde_json::Value = serde_json::from_str(&payload)?;
    let role = claims["role"].as_str().unwrap_or("unknown");
    
    // Method 2: Typed Parsing
    #[derive(Deserialize)]
    struct MyClaims {
        sub: String,
        role: String,
        level: i32,
        #[serde(default)]
        permissions: Vec<String>,
    }
    
    let claims: MyClaims = PasetoParser::<V4, Local>::default()
        .parse_into(&token, &key)?;