# PQC JWT Python Library
Project description
PQC JWT Python Library
A comprehensive Python library for generating, managing, signing, and verifying Post-Quantum Cryptography (PQC) JSON Web Tokens (JWTs). Supports ML-DSA (Dilithium), Falcon, and SPHINCS+ digital signature algorithms via the pqcrypto package.
This library encapsulates key management and JWT logic, making it easy to integrate quantum-resistant signatures into your application's authentication flow.
Features
- Post-Quantum Ready: Implements NIST-standardized ML-DSA, Falcon padded, and SPHINCS+ signature algorithms for quantum-safe JWTs
- Multiple Algorithm Support: Handles multiple security levels and schemes:
- ML-DSA (Dilithium): ML-DSA-44, ML-DSA-65, ML-DSA-87
- Falcon Padded: Falcon-512, Falcon-1024
- SPHINCS+: Multiple variants with SHA2 and SHAKE hashing
- Flexible Key Storage: Save/load key pairs in multiple formats:
pem: Standard-compliant ASN.1 structures (PKCS#8 for private keys, X.509 SPKI for public keys) with official NIST Object Identifiers (OIDs).pub: Simple public key in hex formatbin: Raw binary key bytes
- Publisher/Consumer Roles:
- Publisher: Generates keys and signs JWTs
- Consumer: Loads public keys and verifies JWTs
- Standard JWT Claims Validation: Automatically validates
exp,nbf, andiatclaims - Comprehensive Error Handling: Detailed exceptions for all error scenarios
Requirements
- Python 3.7 or higher
pqcryptopackage
pip install pqcrypto pyasn1
Installation
From PyPI
pip install pqjwt
From Source
git clone https://github.com/eduardogiraudi/pqjwt.git
cd pqjwt
pip install -e .
Quick Start
Basic Usage
import time
from pqjwt import create_publisher, create_consumer
# Publisher: generates keys if missing, signs JWTs
publisher = create_publisher(
key_dir="./keys",
algorithm="ML-DSA-44",
key_format="pem"
)
# Create JWT payload with standard claims
payload = {
"user_id": 123,
"role": "admin",
"iat": int(time.time()), # Issued at
"exp": int(time.time()) + 3600 # Expires in 1 hour
}
# Sign and encode JWT
jwt_token = publisher.encode(payload)
print("Generated JWT:", jwt_token)
# Consumer: loads public key, verifies JWT
consumer = create_consumer(
key_dir="./keys",
algorithm="ML-DSA-44",
key_format="pem"
)
try:
headers, claims = consumer.decode(jwt_token)
print("JWT is valid!")
print("Headers:", headers)
print("Claims:", claims)
except Exception as e:
print("Verification failed:", e)
Advanced Usage with Custom Headers
from pqjwt import create_publisher
publisher = create_publisher(algorithm="FN-DSA-512")
# Custom JWT headers
custom_headers = {
"alg": "FN-DSA-512",
"typ": "JWT",
"kid": "key-001" # Key ID for key rotation
}
payload = {
"sub": "user@example.com",
"scope": "read write admin",
"exp": int(time.time()) + 7200
}
jwt_token = publisher.encode(payload, headers=custom_headers)
print("JWT with custom headers:", jwt_token)
Supported Algorithms
ML-DSA (Dilithium) - NIST Standardized
| Algorithm | JWT Header | Security Level | Description |
|---|---|---|---|
ML-DSA-44 |
ML-DSA-44 |
Level 2 | Balanced security/performance |
ML-DSA-65 |
ML-DSA-65 |
Level 3 | Higher security |
ML-DSA-87 |
ML-DSA-87 |
Level 5 | Highest security |
Falcon Padded - Timing Attack Resistant
| Algorithm | JWT Header | Security Level | Signature Size |
|---|---|---|---|
FN-DSA-512 |
FN-DSA-512 |
Level 1 | ~690 bytes |
FN-DSA-1024 |
FN-DSA-1024 |
Level 5 | ~1330 bytes |
SPHINCS+ - Hash-Based Security
| Algorithm | JWT Header | Security Level | Signature Size |
|---|---|---|---|
SLH-DSA-SHA2-128f |
SLH-DSA-SHA2-128f |
Level 1 | ~17KB |
SLH-DSA-SHA2-128s |
SLH-DSA-SHA2-128s |
Level 1 | ~8KB |
SLH-DSA-SHA2-192f |
SLH-DSA-SHA2-192f |
Level 3 | ~35KB |
SLH-DSA-SHA2-192s |
SLH-DSA-SHA2-192s |
Level 3 | ~16KB |
SLH-DSA-SHA2-256f |
SLH-DSA-SHA2-256f |
Level 5 | ~49KB |
SLH-DSA-SHA2-256s |
SLH-DSA-SHA2-256s |
Level 5 | ~22KB |
Plus SHAKE variants for all SPHINCS+ algorithms
Key Management
Key Formats
PEM Format (Default)
# Keys stored as:
# - ml-dsa-44_public.pem
# - ml-dsa-44_private.pem
publisher = create_publisher(
key_dir="./keys",
algorithm="ML-DSA-44",
key_format="pem" # Default
)
Example PEM file:
-----BEGIN PUBLIC KEY-----
MOCK_BASE64_DATA_HERE
-----END PUBLIC KEY-----
PUB Format (Public Key Only)
# Public key stored as:
# - ml-dsa-44_public.pub
publisher = create_publisher(
key_dir="./keys",
algorithm="ML-DSA-44",
key_format="pub"
)
Example PUB file:
ML-DSA-44 PUBLIC KEY
a1b2c3d4e5f6... (hex encoded key)
BIN Format (Raw Binary)
# Keys stored as raw binary:
# - ml-dsa-44_public.bin
# - ml-dsa-44_private.bin
publisher = create_publisher(
key_dir="./keys",
algorithm="ML-DSA-44",
key_format="bin"
)
Manual Key Management
from pqjwt import JWTKeyManager
# Save keys manually
public_key = b"..." # Raw public key bytes
JWTKeyManager.save_key(
public_key,
"my_key.pem",
format_type="pem",
key_type="public",
algorithm="ML-DSA-44"
)
# Load keys manually
loaded_key, algorithm = JWTKeyManager.load_key(
"my_key.pem",
format_type="pem",
key_type="public"
)
API Reference
JWTManager Class
Main class for JWT operations.
Initialization
JWTManager(
mode="publisher", # "publisher" or "consumer"
key_dir="./keys", # Directory for key storage
key_format="pem", # "pem", "pub", or "bin"
algorithm="ML-DSA-44" # Any supported algorithm
)
Methods
encode(payload, headers=None)
Signs a payload and returns JWT string.
publisher = create_publisher()
jwt = publisher.encode(
payload={"user": "123", "exp": ...},
headers={"alg": "ML-DSA-44", "typ": "JWT"}
)
decode(jwt, validate_claims=True, clock_skew=5)
Verifies JWT and returns (headers, payload).
consumer = create_consumer()
headers, payload = consumer.decode(
jwt_token,
validate_claims=True, # Validate exp, nbf, iat
clock_skew=5 # Allow 5 seconds clock difference
)
verify(jwt)
Quick verification without full decoding.
is_valid = consumer.verify(jwt_token)
get_public_key(output_format="bytes")
Get public key in different formats.
key_bytes = publisher.get_public_key("bytes")
key_hex = publisher.get_public_key("hex")
key_pem = publisher.get_public_key("pem")
export_public_key(file_path, format_type=None)
Export public key to file.
publisher.export_public_key("backup_key.pem", "pem")
Factory Functions
create_publisher(key_dir="./keys", key_format="pem", algorithm="ML-DSA-44")
Creates a JWTManager instance in publisher mode.
create_consumer(key_dir="./keys", key_format="pem", algorithm="ML-DSA-44")
Creates a JWTManager instance in consumer mode.
JWTKeyManager Class
Static utility methods for key management.
get_supported_algorithms()
Returns list of all supported algorithms.
algorithms = JWTKeyManager.get_supported_algorithms()
print("Supported:", algorithms)
get_jwt_header_name(algorithm)
Maps algorithm name to JWT header.
header = JWTKeyManager.get_jwt_header_name("ML-DSA-44")
print(header) # "ML-DSA-44"
get_algorithm_from_jwt_header(jwt_header)
Maps JWT header back to algorithm name.
algorithm = JWTKeyManager.get_algorithm_from_jwt_header("ML-DSA-44")
print(algorithm) # "ML-DSA-44"
Error Handling
The library provides detailed exceptions for all error scenarios:
from pqjwt import (
JWTExpiredError,
JWTSignatureError,
JWTValidationError,
AlgorithmNotSupportedError,
CryptoKeyError
)
try:
headers, payload = consumer.decode(jwt_token)
except JWTExpiredError as e:
print("Token expired:", e)
except JWTSignatureError as e:
print("Invalid signature:", e)
except JWTValidationError as e:
print("Validation failed:", e)
except AlgorithmNotSupportedError as e:
print("Algorithm not supported:", e)
Available Exceptions
JWTExpiredError: Token has expiredJWTSignatureError: Signature verification failedJWTValidationError: General validation failure (malformed token, invalid claims)JWTDecodeError: Error decoding Base64 or JSONCryptoKeyError: Key-related errors (missing private key, etc.)AlgorithmNotSupportedError: Unsupported algorithm specifiedKeyFormatError: Invalid key format
Security Considerations
Algorithm Security
- ML-DSA: NIST Standardized (FIPS 204), no known practical attacks
- Falcon Padded: Mitigates timing attacks present in basic Falcon variants
- SPHINCS+: Conservative hash-based security, very large signatures but resistant to lattice attacks
Key Protection
- Store private keys securely with appropriate file permissions
- Never expose private keys in client applications
- Use different key directories for different environments
Best Practices
# Use appropriate security levels
production_algorithm = "ML-DSA-65" # Level 3 security
testing_algorithm = "ML-DSA-44" # Level 2 security
# Set reasonable expiration times
payload = {
"exp": int(time.time()) + 3600, # 1 hour for access tokens
"iat": int(time.time())
}
Testing
Run the test suite:
python -m pytest tests/ -v
Or run the test file directly:
python tests/test.py
File Naming Convention
Keys are automatically named based on algorithm and format:
| Algorithm | Format | Public Key File | Private Key File |
|---|---|---|---|
| ML-DSA-44 | pem | ml-dsa-44_public.pem |
ml-dsa-44_private.pem |
| FN-DSA-512 | pub | fn-dsa-512.pub |
fn-dsa-512_private.pem |
| SLH-DSA-SHA2-128f | bin | slh-dsa-sha2-128f_public.bin |
slh-dsa-sha2-128f_private.bin |
Note: Private keys are always stored in PEM format when using PUB format for public keys.
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Troubleshooting
Common Issues
"Algorithm not supported" Error
- Check algorithm name spelling
- Use
JWTKeyManager.get_supported_algorithms()to see available options
"Public key not found" Error
- Ensure key files exist in the specified directory
- Check file naming convention matches algorithm
Signature Verification Fails
- Ensure same algorithm used for signing and verification
- Check clock skew for time-based claims
Performance Issues with SPHINCS+
- SPHINCS+ has large signature sizes, consider using ML-DSA or Falcon for high-throughput applications
Support
For bugs and feature requests, please open an issue on GitHub.
For security vulnerabilities, please contact the maintainers directly.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pqjwt-0.2.1.tar.gz.
File metadata
- Download URL: pqjwt-0.2.1.tar.gz
- Upload date:
- Size: 19.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
586cc8457aa1e6d916a0d5ef81632f9a01e024d058e2317678c380bad5b09a63
|
|
| MD5 |
326bba3b64b0c5adff34f84dce23cb59
|
|
| BLAKE2b-256 |
1b0ced53e6d6c4d074669cc038ca50dcb5f3e8fdfd4786237afd49e5dcd293c6
|
File details
Details for the file pqjwt-0.2.1-py3-none-any.whl.
File metadata
- Download URL: pqjwt-0.2.1-py3-none-any.whl
- Upload date:
- Size: 13.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26988ed91b853ace4d30b1c29e81ee0a39a889bbcc14f4d1df70d84fe63d7909
|
|
| MD5 |
f556bea287115d568c90de16b92ea483
|
|
| BLAKE2b-256 |
6adcc94c17714541f50e40834cd0cf6171995a3c9d200c8cd40772ba7d582a24
|