Skip to content

Commit 9143a29

Browse files
committed
Merge branch 'main' of github.com:rrrodzilla/rusty_paseto
2 parents ecf32c7 + 1f94dab commit 9143a29

19 files changed

Lines changed: 443 additions & 440 deletions

.github/workflows/rust.yml

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
on: [push, pull_request]
22
name: Continuous integration
33
jobs:
4-
check:
5-
name: Check
4+
default-features:
5+
name: Default Features (batteries_included + v4)
66
runs-on: ubuntu-latest
77
steps:
88
- uses: actions/checkout@v2
@@ -11,9 +11,16 @@ jobs:
1111
profile: minimal
1212
toolchain: stable
1313
override: true
14-
- uses: actions-rs/cargo@v1
14+
components: clippy
15+
- name: Install nextest
16+
uses: taiki-e/install-action@nextest
17+
- name: Clippy (default features)
18+
uses: actions-rs/cargo@v1
1519
with:
16-
command: check
20+
command: clippy
21+
args: -- -D warnings
22+
- name: Test (default features)
23+
run: cargo nextest run
1724
test:
1825
name: Test Suite
1926
runs-on: ubuntu-latest
@@ -35,10 +42,36 @@ jobs:
3542
profile: minimal
3643
toolchain: stable
3744
override: true
45+
- name: Install nextest
46+
uses: taiki-e/install-action@nextest
47+
- name: Run tests
48+
run: cargo nextest run --no-default-features --features ${{ matrix.feature }}
49+
clippy:
50+
name: Clippy
51+
runs-on: ubuntu-latest
52+
strategy:
53+
matrix:
54+
feature:
55+
- v1_local
56+
- v1_public
57+
- v2_local
58+
- v2_public
59+
- v3_local
60+
- v3_public
61+
- v4_local
62+
- v4_public
63+
steps:
64+
- uses: actions/checkout@v2
65+
- uses: actions-rs/toolchain@v1
66+
with:
67+
profile: minimal
68+
toolchain: stable
69+
override: true
70+
components: clippy
3871
- uses: actions-rs/cargo@v1
3972
with:
40-
command: test
41-
args: --no-default-features --features ${{ matrix.feature }}
73+
command: clippy
74+
args: --no-default-features --features ${{ matrix.feature }} -- -D warnings
4275
audit:
4376
name: Security Audit
4477
runs-on: ubuntu-latest

CONTRIBUTING.md

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,155 @@
11
# Contributing
22

33
When contributing to this repository, please first discuss the change you wish to make via issue,
4-
email, or any other method with the owners of this repository before making a change.
4+
email, or any other method with the owners of this repository before making a change.
55

66
Please note we have a code of conduct, please follow it in all your interactions with the project.
77

8+
## Architecture Overview
9+
10+
### Feature-Gated Design
11+
12+
This crate uses **mutually exclusive features by design**. The PASETO specification recommends choosing a single version per application. This architectural choice:
13+
14+
- Minimizes binary size by only compiling required cryptographic dependencies
15+
- Enforces compile-time safety through intentionally conflicting trait implementations
16+
- Prevents version mixing that could introduce security vulnerabilities
17+
18+
**Important:** `cargo build --all-features` **will fail**. This is intentional, not a bug.
19+
20+
### Feature Combinations
21+
22+
**Valid combinations:**
23+
```bash
24+
# Single version + purpose
25+
--features v4_local
26+
--features v4_public
27+
--features v1_local
28+
29+
# Same version, both purposes
30+
--features "v4_local,v4_public"
31+
32+
# Default (recommended)
33+
# Enables: batteries_included + v4_local + v4_public
34+
```
35+
36+
**Invalid combinations (will fail at compile time):**
37+
```bash
38+
# Multiple public features
39+
--features "v1_public,v2_public" # ❌ Trait conflict
40+
--features "v3_public,v4_public" # ❌ Trait conflict
41+
42+
# All features
43+
--all-features # ❌ Enables conflicting features
44+
```
45+
46+
See [Issue #48](https://github.com/rrrodzilla/rusty_paseto/issues/48) for details.
47+
48+
## Development Setup
49+
50+
### Required Tools
51+
52+
```bash
53+
# Install cargo-nextest (used by CI and local development)
54+
cargo install cargo-nextest
55+
```
56+
57+
### Testing
58+
59+
**Match CI behavior** by testing each feature combination individually:
60+
61+
```bash
62+
# Test individual features (matches CI matrix)
63+
cargo nextest run --no-default-features --features v1_local
64+
cargo nextest run --no-default-features --features v2_local
65+
cargo nextest run --no-default-features --features v3_local
66+
cargo nextest run --no-default-features --features v4_local
67+
cargo nextest run --no-default-features --features v1_public
68+
cargo nextest run --no-default-features --features v2_public
69+
cargo nextest run --no-default-features --features v3_public
70+
cargo nextest run --no-default-features --features v4_public
71+
72+
# Test default features
73+
cargo nextest run
74+
```
75+
76+
**Do NOT use:**
77+
```bash
78+
cargo test --all-features # ❌ Will fail
79+
cargo nextest run --all-features # ❌ Will fail
80+
```
81+
82+
### Building
83+
84+
```bash
85+
# Build with specific features
86+
cargo build --no-default-features --features v4_local
87+
88+
# Build with default features
89+
cargo build
90+
```
91+
92+
### Linting
93+
94+
```bash
95+
# Run clippy (zero tolerance for warnings)
96+
cargo clippy --all-targets --features v4_local -- -D warnings
97+
cargo clippy --all-targets --features v4_public -- -D warnings
98+
99+
# Format code
100+
cargo fmt
101+
```
102+
103+
## Feature Architecture Layers
104+
105+
The crate has three architectural layers:
106+
107+
1. **`core`** - Bare PASETO cryptographic primitives (no serde, minimal deps)
108+
2. **`generic`** - Customizable builder/parser foundation (adds serde, claims)
109+
3. **`batteries_included`** - Ready-to-use builders with sensible defaults
110+
111+
Version/purpose combinations:
112+
- `v1_local`, `v2_local`, `v3_local`, `v4_local` - Symmetric encryption
113+
- `v1_public`, `v2_public`, `v3_public`, `v4_public` - Asymmetric signing
114+
115+
## Contribution Guidelines
116+
117+
### Before Submitting
118+
119+
1. **Discuss first** - Open an issue before starting work
120+
2. **Test all affected features** - Run the feature-specific tests shown above
121+
3. **Check clippy** - Zero warnings policy
122+
4. **Format code** - Run `cargo fmt`
123+
5. **Update docs** - If adding features or changing public APIs
124+
125+
### Pull Request Process
126+
127+
1. Ensure tests pass for all affected feature combinations
128+
2. Update CHANGELOG.md if applicable
129+
3. Follow conventional commit format: `type(scope): description`
130+
4. Reference related issues with `Closes #123` or `Addresses #123`
131+
132+
### Understanding Test Failures
133+
134+
If your PR fails CI with trait conflicts:
135+
- Check if you're enabling multiple public features
136+
- Verify your changes don't break feature isolation
137+
- Test locally with the exact feature combination that failed
138+
139+
### Common Mistakes
140+
141+
**Don't:** Try to make `--all-features` work
142+
**Do:** Understand this is intentional architectural design
143+
144+
**Don't:** Test only with default features
145+
**Do:** Test all affected feature combinations individually
146+
147+
**Don't:** Add dependencies without feature gates
148+
**Do:** Make new dependencies optional and feature-gated
149+
150+
**Don't:** Implement traits that conflict across versions
151+
**Do:** Keep version-specific code isolated
152+
153+
## Questions?
154+
155+
Open an issue or reach out to the maintainers before starting work.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ aes = { version = "0.8", optional = true }
7171
ctr = { version = "0.9", optional = true }
7272
hmac = { version = "0.12", optional = true }
7373
sha2 = { version = "0.10", optional = true }
74+
subtle = "2.6"
7475
zeroize = { version = "1.4", features = ["zeroize_derive"] }
7576
time = { version = "0.3", features = ["parsing", "formatting"] }
7677
rand_core = "0.9"

readme.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,45 @@ use rusty_paseto::prelude::*;
346346

347347
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:
348348

349+
### ⚠️ Feature-Gated Design - Important
350+
351+
**This crate uses mutually exclusive features by design.** The PASETO specification recommends choosing a single version per application. This architectural choice:
352+
353+
- **Minimizes binary size** by only compiling required cryptographic dependencies
354+
- **Enforces compile-time safety** through intentionally conflicting trait implementations
355+
- **Prevents version mixing** that could introduce security vulnerabilities
356+
357+
**Important:** Building with `--all-features` **will fail**. This is intentional, not a bug.
358+
359+
#### Valid Feature Combinations
360+
361+
```toml
362+
# Single version + purpose ✅
363+
rusty_paseto = { version = "latest", features = ["v4_local"] }
364+
rusty_paseto = { version = "latest", features = ["v4_public"] }
365+
366+
# Same version, both purposes ✅
367+
rusty_paseto = { version = "latest", features = ["v4_local", "v4_public"] }
368+
369+
# Default (recommended) ✅
370+
# Enables: batteries_included + v4_local + v4_public
371+
rusty_paseto = "latest"
372+
```
373+
374+
#### Invalid Feature Combinations
375+
376+
```toml
377+
# Multiple public features ❌ (Trait conflict)
378+
rusty_paseto = { version = "latest", features = ["v1_public", "v2_public"] }
379+
rusty_paseto = { version = "latest", features = ["v3_public", "v4_public"] }
380+
```
381+
382+
See [Issue #48](https://github.com/rrrodzilla/rusty_paseto/issues/48) for technical details.
383+
384+
<h6 align="right"><a href="#user-content-table-of-contents">back to toc</a></h6>
385+
386+
### Architecture Layers
387+
349388
<img src="https://github.com/rrrodzilla/rusty_paseto/raw/main/assets/RustyPasetoPreludeArchitecture.png" width="150" /> <img src="https://github.com/rrrodzilla/rusty_paseto/raw/main/assets/RustyPasetoGenericArchitecture.png" width="150" /> <img src="https://github.com/rrrodzilla/rusty_paseto/raw/main/assets/RustyPasetoCoreArchitecture.png" width="150" />
350389

351390
batteries_included --> generic --> core

src/core/common/pre_authentication_encoding.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ pub struct PreAuthenticationEncoding(Vec<u8>);
77
///
88
impl PreAuthenticationEncoding {
99
/// * `pieces` - The Pieces to concatenate, and encode together.
10-
/// Refactored from original code found at
11-
/// <https://github.com/instructure/paseto/blob/trunk/src/pae.rs>
10+
/// Refactored from original code found at
11+
/// <https://github.com/instructure/paseto/blob/trunk/src/pae.rs>
1212
pub fn parse<'a>(pieces: &'a [&'a [u8]]) -> Self {
1313
let the_vec = PreAuthenticationEncoding::le64(pieces.len() as u64);
1414

@@ -21,7 +21,7 @@ impl PreAuthenticationEncoding {
2121
/// Encodes a u64-bit unsigned integer into a little-endian binary string.
2222
///
2323
/// * `to_encode` - The u8 to encode.
24-
/// Copied and gently refactored from <https://github.com/instructure/paseto/blob/trunk/src/pae.rs>
24+
/// Copied and gently refactored from <https://github.com/instructure/paseto/blob/trunk/src/pae.rs>
2525
pub(crate) fn le64(mut to_encode: u64) -> Vec<u8> {
2626
let mut the_vec = Vec::with_capacity(8);
2727

src/core/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
//! }
4444
//! # Ok::<(),anyhow::Error>(())
4545
//! ```
46-
4746
mod error;
4847
mod footer;
4948
mod header;

src/core/paseto_impl/v1_local.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#![cfg(feature = "v1_local")]
22
use std::str;
33
use hmac::{Hmac, Mac};
4+
use subtle::ConstantTimeEq;
45
use crate::core::{Footer, Header, Key, Local, Paseto, PasetoError, PasetoNonce, PasetoSymmetricKey, V1};
56
use crate::core::common::{AuthenticationKey, AuthenticationKeySeparator, CipherText, EncryptionKey, EncryptionKeySeparator, PreAuthenticationEncoding, RawPayload, Tag};
6-
use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals;
77
use sha2::Sha384;
88

99
impl<'a> Paseto<'a, V1, Local> {
@@ -26,7 +26,7 @@ impl<'a> Paseto<'a, V1, Local> {
2626
pub fn try_decrypt(
2727
token: &'a str,
2828
key: &PasetoSymmetricKey<V1, Local>,
29-
footer: (impl Into<Option<Footer<'a>>> + Copy),
29+
footer: impl Into<Option<Footer<'a>>> + Copy,
3030
) -> Result<String, PasetoError> {
3131
let decoded_payload = Self::parse_raw_token(token, footer, &V1::default(), &Local::default())?;
3232
let nonce = Key::from(&decoded_payload[..32]);
@@ -51,7 +51,9 @@ impl<'a> Paseto<'a, V1, Local> {
5151
let tag = &decoded_payload[(nonce.len() + ciphertext.len())..];
5252
let tag2 = &Tag::<V1, Local>::from(authentication_key, &pae);
5353
//compare tags
54-
ConstantTimeEquals(tag, tag2)?;
54+
if !bool::from(tag.ct_eq(tag2.as_ref())) {
55+
return Err(PasetoError::Cryption);
56+
}
5557

5658
//decrypt payload
5759
let ciphertext = CipherText::<V1, Local>::from(ciphertext, &encryption_key);

src/core/paseto_impl/v1_public.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ impl<'a> Paseto<'a, V1, Public> {
99
pub fn try_verify(
1010
signature: &'a str,
1111
public_key: &PasetoAsymmetricPublicKey<V1, Public>,
12-
footer: (impl Into<Option<Footer<'a>>> + Copy),
12+
footer: impl Into<Option<Footer<'a>>> + Copy,
1313
) -> Result<String, PasetoError> {
1414
let decoded_payload = Self::parse_raw_token(signature, footer, &V1::default(), &Public::default())?;
1515

src/core/paseto_impl/v2_local.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl<'a> Paseto<'a, V2, Local> {
2525
pub fn try_decrypt(
2626
token: &'a str,
2727
key: &PasetoSymmetricKey<V2, Local>,
28-
footer: (impl Into<Option<Footer<'a>>> + Copy),
28+
footer: impl Into<Option<Footer<'a>>> + Copy,
2929
) -> Result<String, PasetoError> {
3030
//get footer
3131

src/core/paseto_impl/v2_public.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ impl<'a> Paseto<'a, V2, Public> {
1010
pub fn try_verify(
1111
signature: &'a str,
1212
public_key: &PasetoAsymmetricPublicKey<V2, Public>,
13-
footer: (impl Into<Option<Footer<'a>>> + Copy),
13+
footer: impl Into<Option<Footer<'a>>> + Copy,
1414
) -> Result<String, PasetoError> {
1515
let decoded_payload = Self::parse_raw_token(signature, footer, &V2::default(), &Public::default())?;
1616

0 commit comments

Comments
 (0)