Code Monkey home page Code Monkey logo

rusty_paseto's Introduction

rusty_paseto

A type-driven, ergonomic implementation of the PASETO protocol
for secure stateless tokens.

unit tests GitHub unsafe forbidden Crates.io Documentation


Table of Contents

  1. About PASETO
  2. Examples
  3. Architecture
  4. Roadmap and Current Feature Status
  5. Acknowledgments
  6. Support
  7. Like this crate

About PASETO

PASETO: Platform-Agnostic Security Tokens

Paseto is everything you love about JOSE (JWT, JWE, JWS) without any of the many design deficits that plague the JOSE standards.

rusty_paseto is meant to be flexible and configurable for your specific use case. Whether you want to get started quickly with sensible defaults, create your own version of rusty_paseto in order to customize your own defaults and functionality or just want to use the core PASETO crypto features, the crate is heavily feature gated to allow for your needs.

Examples

Building and parsing tokens

Here's a basic, default token with the batteries_included feature:

use rusty_paseto::prelude::*;

// create a key specifying the PASETO version and purpose
let key = PasetoSymmetricKey::<V4, Local>::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
// use a default token builder with the same PASETO version and purpose
let token = PasetoBuilder::<V4, Local>::default().build(&key)?;
// token is a String in the form: "v4.local.encoded-payload"

A default token

  • Has no footer
  • Has no implicit assertion for V3 or V4 versioned tokens
  • Expires in 1 hour after creation (due to an included default ExpirationClaim)
  • Contains an IssuedAtClaim defaulting to the current utc time the token was created
  • Contains a NotBeforeClaim defaulting to the current utc time the token was created

You can parse and validate an existing token with the following:

let key = PasetoSymmetricKey::<V4, Local>::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
// now we can parse and validate the token with a parser that returns a serde_json::Value
let json_value = PasetoParser::<V4, Local>::default().parse(&token, &key)?;

//the ExpirationClaim
assert!(json_value["exp"].is_string());
//the IssuedAtClaim
assert!(json_value["iat"].is_string());

A default parser

  • Validates the token structure and decryptes the payload or verifies the signature of the content
  • Validates the footer if one was provided
  • Validates the implicit assertion if one was provided (for V3 or V4 versioned tokens only)

A token with a footer

PASETO tokens can have an optional footer. In rusty_paseto we have strict types for most things. So we can extend the previous example to add a footer to the token by using code like the following:

use rusty_paseto::prelude::*;
let key = PasetoSymmetricKey::<V4, Local>::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
let token = PasetoBuilder::<V4, Local>::default()
  // note how we set the footer here
  .set_footer(Footer::from("Sometimes science is more art than science"))
  .build(&key)?;

// token is now a String in the form: "v4.local.encoded-payload.footer"

And parse it by passing in the same expected footer

// now we can parse and validate the token with a parser that returns a serde_json::Value
let json_value = PasetoParser::<V4, Local>::default()
  .set_footer(Footer::from("Sometimes science is more art than science"))
  .parse(&token, &key)?;

//the ExpirationClaim
assert!(json_value["exp"].is_string());
//the IssuedAtClaim
assert!(json_value["iat"].is_string());

A token with an implicit assertion (V3 or V4 versioned tokens only)

Version 3 (V3) and Version 4 (V4) PASETO tokens can have an optional implicit assertion. So we can extend the previous example to add an implicit assertion to the token by using code like the following:

use rusty_paseto::prelude::*;
let key = PasetoSymmetricKey::<V4, Local>::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
let token = PasetoBuilder::<V4, Local>::default()
  .set_footer(Footer::from("Sometimes science is more art than science"))
  // note how we set the implicit assertion here
  .set_implicit_assertion(ImplicitAssertion::from("There’s a lesson here, and I’m not going to be the one to figure it out."))
  .build(&key)?;

// token is now a String in the form: "v4.local.encoded-payload.footer"

And parse it by passing in the same expected implicit assertion at parse time

// now we can parse and validate the token with a parser that returns a serde_json::Value
let json_value = PasetoParser::<V4, Local>::default()
  .set_footer(Footer::from("Sometimes science is more art than science"))
  .set_implicit_assertion(ImplicitAssertion::from("There’s a lesson here, and I’m not going to be the one to figure it out."))
  .parse(&token, &key)?;

Setting a different expiration time

As mentioned, default tokens expire 1 hour from creation time. You can set your own expiration time by adding an ExpirationClaim which takes an ISO 8601 (Rfc3339) compliant datetime string.

Note: claims taking an ISO 8601 (Rfc3339) string use the TryFrom trait and return a Result<(),PasetoClaimError>

use rusty_paseto::prelude::*;
// must include
use std::convert::TryFrom;
let key = PasetoSymmetricKey::<V4, Local>::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
// real-world example using the time crate to expire 5 minutes from now

let token = PasetoBuilder::<V4, Local>::default()
  // note the TryFrom implmentation for ExpirationClaim
  //.set_claim(ExpirationClaim::try_from("2019-01-01T00:00:00+00:00")?)
  .set_claim(ExpirationClaim::try_from(in_5_minutes)?)
  .set_footer(Footer::from("Sometimes science is more art than science"))
  .build(&key)?;

// token is a String in the form: "v4.local.encoded-payload.footer"

Tokens that never expire

A 1 hour ExpirationClaim is set by default because the use case for non-expiring tokens in the world of security tokens is fairly limited. Omitting an expiration claim or forgetting to require one when processing them is almost certainly an oversight rather than a deliberate choice.

When it is a deliberate choice, you have the opportunity to deliberately remove this claim from the Builder. The method call required to do so ensures readers of the code understand the implicit risk.

let token = PasetoBuilder::<V4, Local>::default()
  .set_claim(ExpirationClaim::try_from(in_5_minutes)?)
  // even if you set an expiration claim (as above) it will be ignored
  // due to the method call below
  .set_no_expiration_danger_acknowledged()
  .build(&key)?;

Setting PASETO Claims

The PASETO specification includes seven reserved claims which you can set with their explicit types:

// real-world example using the time crate to prevent the token from being used before 2
// minutes from now
let in_2_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(2)).format(&Rfc3339)?;

let token = PasetoBuilder::<V4, Local>::default()
  //json payload key: "exp"
  .set_claim(ExpirationClaim::try_from(in_5_minutes)?)
  //json payload key: "iat"
  // the IssueAtClaim is automatically set to UTC NOW by default
  // but you can override it here
  // .set_claim(IssuedAtClaim::try_from("2019-01-01T00:00:00+00:00")?)
  //json payload key: "nbf"
  //don't use this token before two minutes after UTC NOW
  .set_claim(NotBeforeClaim::try_from(in_2_minutes)?)
  //json payload key: "aud"
  .set_claim(AudienceClaim::from("Cromulons"))
  //json payload key: "sub"
  .set_claim(SubjectClaim::from("Get schwifty"))
  //json payload key: "iss"
  .set_claim(IssuerClaim::from("Earth Cesium-137"))
  //json payload key: "jti"
  .set_claim(TokenIdentifierClaim::from("Planet Music - Season 988"))
  .build(&key)?;

Setting your own Custom Claims

The CustomClaim struct takes a tuple in the form of (key: String, value: T) where T is any serializable type

Note: CustomClaims use the TryFrom trait and return a Result<(), PasetoClaimError> if you attempt to use one of the reserved PASETO keys in your CustomClaim

let token = PasetoBuilder::<V4, Local>::default()
  .set_claim(CustomClaim::try_from(("Co-star", "Morty Smith"))?)
  .set_claim(CustomClaim::try_from(("Universe", 137))?)
  .build(&key)?;

This throws an error:

// "exp" is a reserved PASETO claim key, you should use the ExpirationClaim type
let token = PasetoBuilder::<V4, Local>::default()
  .set_claim(CustomClaim::try_from(("exp", "Some expiration value"))?)
  .build(&key)?;

Validating claims

rusty_paseto allows for flexible claim validation at parse time

Checking claims

Let's see how we can check particular claims exist with expected values.

// use a default token builder with the same PASETO version and purpose
let token = PasetoBuilder::<V4, Local>::default()
  .set_claim(SubjectClaim::from("Get schwifty"))
  .set_claim(CustomClaim::try_from(("Contestant", "Earth"))?)
  .set_claim(CustomClaim::try_from(("Universe", 137))?)
  .build(&key)?;

PasetoParser::<V4, Local>::default()
  // you can check any claim even custom claims
  .check_claim(SubjectClaim::from("Get schwifty"))
  .check_claim(CustomClaim::try_from(("Contestant", "Earth"))?)
  .check_claim(CustomClaim::try_from(("Universe", 137))?)
  .parse(&token, &key)?;

// no need for the assertions below since the check_claim methods
// above accomplish the same but at parse time!

//assert_eq!(json_value["sub"], "Get schwifty");
//assert_eq!(json_value["Contestant"], "Earth");
//assert_eq!(json_value["Universe"], 137);

Custom validation

What if we have more complex validation requirements? You can pass in a reference to a closure which receives the key and value of the claim you want to validate so you can implement any validation logic you choose.

Let's see how we can validate our tokens only contain universe values with prime numbers:

// use a default token builder with the same PASETO version and purpose
let token = PasetoBuilder::<V4, Local>::default()
  .set_claim(SubjectClaim::from("Get schwifty"))
  .set_claim(CustomClaim::try_from(("Contestant", "Earth"))?)
  .set_claim(CustomClaim::try_from(("Universe", 137))?)
  .build(&key)?;

PasetoParser::<V4, Local>::default()
  .check_claim(SubjectClaim::from("Get schwifty"))
  .check_claim(CustomClaim::try_from(("Contestant", "Earth"))?)
   .validate_claim(CustomClaim::try_from("Universe")?, &|key, value| {
     //let's get the value
     let universe = value
       .as_u64()
       .ok_or(PasetoClaimError::Unexpected(key.to_string()))?;
     // we only accept prime universes in this app
     if primes::is_prime(universe) {
       Ok(())
     } else {
       Err(PasetoClaimError::CustomValidation(key.to_string()))
     }
   })
  .parse(&token, &key)?;

This token will fail to parse with the validation code above:

// 136 is not a prime number
let token = PasetoBuilder::<V4, Local>::default()
  .set_claim(CustomClaim::try_from(("Universe", 136))?)
  .build(&key)?;

Architecture

The rusty_paseto crate architecture is composed of three layers (batteries_included, generic and core) which can be further refined by the PASETO version(s) and purpose(s) required for your needs. All layers use a common crypto core which includes various cipher crates depending on the version and purpose you choose. The crate is heavily featured gated to allow you to use only the versions and purposes you need for your app which minimizes download compile times for using rusty_paseto. A description of each architectural layer, their uses and limitations and how to minimize your required dependencies based on your required PASETO version and purpose follows:

batteries_included --> generic --> core

Feature gates

Valid version/purpose feature combinations are as follows:

  • "v1_local" (NIST Original Symmetric Encryption)
  • "v2_local" (Sodium Original Symmetric Encryption)
  • "v3_local" (NIST Modern Symmetric Encryption)
  • "v4_local" (Sodium Modern Symmetric Encryption)
  • "v1_public" (NIST Original Asymmetric Authentication)
  • "v2_public" (Sodium Original Asymmetric Authentication)
  • "v3_public" (NIST Modern Asymmetric Authentication)
  • "v4_public" (Sodium Modern Asymmetric Authentication)

default

The default feature is the quickest way to get started using rusty_paseto.

The default feature includes the outermost architectural layer called batteries_included (described below) as well as the two latest PASETO versions (V3 - NIST MODERN, V4 - SODIUM MODERN) and the Public (Asymmetric) and Local (Symmetric) purposed key types for each of these versions. That should be four specific version and purpose combinations however at the time of this writing I have yet to implement the V3 - Public combination, so there are 3 in the default feature. Additionally, this feature includes JWT style claims and business rules for your PASETO token (default, but customizable expiration, issued at, not-before times, etc as described in the usage documentation and examples further below).

## Includes V3 (local) and V4 (local, public) versions, 
## purposes and ciphers.

rusty_paseto = "latest"
// at the top of your source file
use rusty_paseto::prelude::*;

batteries_included

The outermost architectural layer is called batteries_included. This is what most people will need. This feature includes JWT style claims and business rules for your PASETO token (default, but customizable expiration, issued at, not-before times, etc as described in the usage documentation and examples below).

You must specify a version and purpose with this feature in order to reduce the size of your dependencies like in the following Cargo.toml entry which only includes the V4 - Local types with batteries_included functionality:
## Includes only v4 modern sodium cipher crypto core 
## and local (symmetric) key types with all claims and default business rules.

rusty_paseto = {version = "latest", features = ["batteries_included", "v4_local"] }

// at the top of your source file
use rusty_paseto::prelude::*;

generic

The generic architectural and feature layer allows you to create your own custom version of the batteries_included layer by following the same pattern I've used in the source code to create your own custom builder and parser. This is probably not what you need as it is for advanced usage. The feature includes a generic builder and parser along with claims for you to extend.

It includes all the PASETO and custom claims but allows you to create different default claims in your custom builder and parser or use a different time crate or make up your own default business rules. As with the batteries_included layer, parsed tokens get returned as a serder_json Value. Again, specify the version and purpose to include in the crypto core:

## Includes only v4 modern sodium cipher crypto core and local (symmetric)
## key types with all claims and default business rules.

rusty_paseto = {version = "latest", features = ["generic", "v4_local"] }
// at the top of your source file
use rusty_paseto::generic::*;

core

The core architectural layer is the most basic PASETO implementation as it accepts a Payload, optional Footer and (if v3 or v4) an optional Implicit Assertion along with the appropriate key to encrypt/sign and decrypt/verify basic strings.

There are no default claims or included claim structures, business rules or anything other than basic PASETO crypto functions. Serde crates are not included in this feature so it is extremely lightweight. You can use this when you don't need JWT-esque functionality but still want to leverage the safe cipher combinations and algorithm lucidity afforded by the PASETO specification.

## Includes only v4 modern sodium cipher crypto core and local (symmetric)
## key types with NO claims, defaults or validation, just basic PASETO
## encrypt/signing and decrypt/verification.

rusty_paseto = {version = "latest", features = ["core", "v4_local"] }

Roadmap and Current Feature Status

PASETO specification

APIs, Tests & Documentation v1.L v1.P v2.L v2.P v3.L v3.P v4.L v4.P
PASETO Token Builder 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
PASETO Token Parser 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Flexible Claim Validation 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Generic Token Builder 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Generic Token Parser 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Encryption/Signing 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Decryption/Verification 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
PASETO Test vectors 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Feature - core 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Feature - generic 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Feature - batteries_included 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Docs - core 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Docs - generic 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
Docs - batteries_included 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢

🟢 - completed ⚫ - planned

PASERK specification

lid local seal local-wrap local-pw sid public pid secret secret-wrap secret-pw

🟢 - completed ⚫ - planned


Support

File an issue or hit me up on Twitter!


Acknowledgments

If the API of this crate doesn't suit your tastes, check out the other PASETO implementations in the Rust ecosystem which inspired rusty_paseto:


Like this crate?

⭐ Star https://github.com/rrrodzilla/rusty_paseto

🐦 Follow https://twitter.com/rrrodzilla


readme created with cargo-markdown

rusty_paseto's People

Contributors

ebbdrop avatar rrrodzilla avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

rusty_paseto's Issues

Beginner question

Hey hey,

really nice implementation and an amazing documentation!

I'm new to rust and my problem might just stem from the fact that I do not know how to archive this the rust-way.

I'm trying to set a subject claim from a non-static reference/dynamic value in a trait implementation.

let token = PasetoBuilder::<V4, Local>::default()
  .set_claim(SubjectClaim::from(&id))
  .build(&key)
  .unwrap();

From what I understand the signature of set_claim required the argument to have a 'static lifetime which will not be possible for a dynamic value (I could be wrong).

How could this be archived?

plans for v3.public

What is your plans re v3.public will you be supporting it? If so do you have an ETA?

Add closure to claim validation API

Currently, claim validation is a simple check that the claim exists in the decrypted token and the decrypted value matches the provided value. The API needs to be extended to allow the user to provide their own validation closure in order to support more complex validation scenarios.

Docs PasetoParser::<V4, Local>

Hello, could you please add additional information to the docs that if token is expired, then PasetoParser::<V4, Local> will throw an error? That would be cool, because I was trying to find a way to verify token itself. It's a good design choice, but that was not mentioned in docs, or I'm simply blind (sorry 😢)

Implement Paseto Token Builder

The generic builder handles building and enforcing paseto structure (claims, etc) but does not enforce paseto logic such as providing a default expiration claim and issue claim while allowing for overrides and non-expiring tokens. The Paseto Token Builder should wrap the generic builder with the custom paseto logic.

Implement Paseto Token Parser

The generic parser handles decrypting the paseto structure (claims, etc) but does not enforce paseto logic such as validating expiration claim and other time related claims. The Paseto Token Parser should wrap the generic parser with the custom paseto logic.

Implement Non-Expiring Token

The user needs to be able to explicitly specify when a token should NOT have an expiration date when building a claim.

Stray eprintln! in `GenericBuilder::set_implicit_assertion`

Describe the bug
Every time set_implicit_assertion on GenericBuilder is used, the assertion is printed to stdout.

To Reproduce

PasetoBuilder::<V4, Local>::default().set_implicit_assertion(ImplicitAssertion::from("example"));

Expected behavior
For a library to not write to stdout.

Wrong pasteo token cause runtime panic

Describe the bug
While testing i discovered a case where the library will panic instead of throwing an error. Explicitly while attempting a wrong token "v4.local.1234".

The error is

rusty_paseto-0.6.0/src/core/paseto.rs:766:47:
range end index 32 out of range for slice of length 3

To Reproduce
just try to decrypt "v4.local.1234"

Expected behavior
i was expecting to catch the error same as other errors.

Additional context
I think this problem could be solved by checking the length of the third part. I am interested to create a PR for this as first contribution.

De-dupe claim keys in builder

Claim keys should only appear once in the top-level JSON payload of the builder to prevent the user from accidentally overwriting claim data in a single build session which would cause confusion. Users should groups of duplicate keys in a child struct with a unique claim key to enable that scenario.

ed25519-dalek vulnerability

Describe the bug
Found vulnerability when ruining cargo audit.

To Reproduce
Steps to reproduce the behavior:

  1. just run cargo audit.

Expected behavior
the vulnerability can be resolved by bumping up the ed25519-dalek crate version.

Screenshots
Screenshot from 2023-11-05 23-04-20

Desktop (please complete the following information):

  • OS: Fedora
  • Browser chrome

Project still active?

There aren't updates, commits or activity in 6 months.
Is the project still active and/or mantained?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.