PySETO is a PASETO (Platform-Agnostic SEcurity TOkens)/PASERK (Platform-Agnostic Serialized Keys) implementation written in Python
which supports all of the versions (v1,
v2,
v3 and
v4) and purposes (public and local)
and has passed all of the official tests.
You can install PySETO with pip:
$ pip install pysetoPySETO can be used in ease as follows (in case of v4.public):
import pyseto
from pyseto import Key
private_key_pem = b"-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----"
public_key_pem = b"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----"
# Create a PASETO token.
private_key = Key.new(version=4, purpose="public", key=private_key_pem)
token = pyseto.encode(
private_key,
b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}',
)
# Decode and verify a PASETO token.
public_key = Key.new(version=4, purpose="public", key=public_key_pem)
decoded = pyseto.decode(public_key, token)
assert (
token
== b"v4.public.eyJkYXRhIjogInRoaXMgaXMgYSBzaWduZWQgbWVzc2FnZSIsICJleHAiOiAiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9l1YiKei2FESvHBSGPkn70eFO1hv3tXH0jph1IfZyEfgm3t1DjkYqD5r4aHWZm1eZs_3_bZ9pBQlZGp0DPSdzDg"
)
assert (
decoded.payload
== b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)See following contents or Documentation for details.
- Installation
- Supported PASETO Versions
- Supported PASERK Types
- PASETO Usage
- PASERK Usage
- API Reference
- Tests
- Security
- Contributing
You can install PySETO with pip:
$ pip install pysetoPySETO supports all of PASETO versions and purposes below:
| v4 | v3 | v2 | v1 | |
|---|---|---|---|---|
local |
✅ | ✅ | ✅ | ✅ |
public |
✅ | ✅ | ✅ | ✅ |
PySETO also supports PASERK (Platform-Agnostic Serialized Keys). Currently, following PASERK types are supported:
| v4 | v3 | v2 | v1 | |
|---|---|---|---|---|
lid |
✅ | ✅ | ✅ | ✅ |
sid |
✅ | ✅ | ✅ | ✅ |
pid |
✅ | ✅ | ✅ | ✅ |
local |
✅ | ✅ | ✅ | ✅ |
secret |
✅ | ✅ | ✅ | ✅ |
public |
✅ | ✅ | ✅ | ✅ |
seal |
✅ | ✅ | ✅ | ✅ |
local-wrap |
✅ | ✅ | ✅ | ✅ |
secret-wrap |
✅ | ✅ | ✅ | ✅ |
local-pw |
✅ | ✅ | ✅ | ✅ |
secret-pw |
✅ | ✅ | ✅ | ✅ |
By using this PySETO, you can easily create, decode and verify PASETO tokens. Here are sample codes that handle version 4 PySETO tokens.
Please refer to the Documentation for all usage examples including other versions.
v4.public is one of current PASETO versions to be used for asymmetric authentication (public key signatures).
import pyseto
from pyseto import Key
private_key_pem = b"-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----"
public_key_pem = b"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----"
private_key = Key.new(version=4, purpose="public", key=private_key_pem)
token = pyseto.encode(
private_key,
b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}',
)
public_key = Key.new(version=4, purpose="public", key=public_key_pem)
decoded = pyseto.decode(public_key, token)
assert (
token
== b"v4.public.eyJkYXRhIjogInRoaXMgaXMgYSBzaWduZWQgbWVzc2FnZSIsICJleHAiOiAiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9l1YiKei2FESvHBSGPkn70eFO1hv3tXH0jph1IfZyEfgm3t1DjkYqD5r4aHWZm1eZs_3_bZ9pBQlZGp0DPSdzDg"
)
assert (
decoded.payload
== b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)v4.local is one of current PASETO versions to be used for symmetric authenticated encryption.
import pyseto
from pyseto import Key
key = Key.new(version=4, purpose="local", key=b"our-secret")
token = pyseto.encode(
key, b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)
decoded = pyseto.decode(key, token)
assert (
decoded.payload
== b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)By using serializer and deserializer, you can encode/decode a dict-typed payload and footer included in PASETO tokens into an arbitrary format.
The following example shows that the payload and the footer in a PASETO token are encoded/decoded as JSON formatted data.
When specifing dict-typed payload, exp parameter can be used to set the expiration time (seconds) of the token.
import json
import pyseto
from pyseto import Key
private_key_pem = b"-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----"
public_key_pem = b"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----"
private_key = Key.new(version=4, purpose="public", key=private_key_pem)
public_key = Key.new(version=4, purpose="public", key=public_key_pem)
token = pyseto.encode(
private_key,
{"data": "this is a signed message"},
footer={"kid": public_key.to_paserk_id()},
serializer=json,
exp=3600,
)
decoded = pyseto.decode(public_key, token, deserializer=json)
assert decoded.payload["data"] == "this is a signed message"
assert decoded.payload["exp"] == "2021-11-11T00:00:00+00:00"
assert decoded.footer["kid"] == "k4.pid.yh4-bJYjOYAG6CWy0zsfPmpKylxS7uAWrxqVmBN2KAiJ"By using Paseto class, you can change the default value of exp (the expiration date ot tokens), whether to include an iat claim, and other settings.
Note that pyseto.encode() and pyseto.decode() are aliases to the encode() and decode() of the global "Paseto" class instance created with the default settings.
import json
import pyseto
from pyseto import Key, Paseto
private_key_pem = b"-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----"
public_key_pem = b"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----"
private_key = Key.new(version=4, purpose="public", key=private_key_pem)
paseto = Paseto.new(
exp=3600, include_iat=True
) # Default values are exp=0(not specified) and including_iat=False
token = paseto.encode(
private_key,
{"data": "this is a signed message"},
serializer=json,
)
public_key = Key.new(version=4, purpose="public", key=public_key_pem)
decoded = pyseto.decode(public_key, token, deserializer=json)
assert decoded.payload["data"] == "this is a signed message"
assert decoded.payload["iat"] == "2021-11-11T00:00:00+00:00"
assert decoded.payload["exp"] == "2021-11-11T01:00:00+00:00"PASERK (Platform-Agnostic Serialized Keys) is an extension to PASETO that provides key-wrapping and serialization.
As shown in the examples above, the pyseto.Key used for encryption and signature can be generated from PASERK or converted to PASERK as follows:
import pyseto
from pyseto import Key
# pyseto.Key can be generated from PASERK.
symmetric_key = Key.new(version=4, purpose="local", key=b"our-secret")
private_key = Key.from_paserk(
"k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog"
)
public_key = Key.from_paserk("k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI")
token = pyseto.encode(
private_key,
b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}',
)
decoded = pyseto.decode(public_key, token)
assert (
decoded.payload
== b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)
# PASERK can be derived from pyseto.Key.
assert symmetric_key.to_paserk() == "k4.local.b3VyLXNlY3JldA"
assert (
private_key.to_paserk()
== "k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog"
)
assert public_key.to_paserk() == "k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI"pyseto.Key can also be converted to PASERK ID as follows:
import pyseto
from pyseto import Key
# pyseto.Key can be generated from PASERK.
symmetric_key = Key.new(version=4, purpose="local", key=b"our-secret")
private_key = Key.from_paserk(
"k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog"
)
public_key = Key.from_paserk("k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI")
# PASERK ID can be derived from pyseto.Key.
assert (
symmetric_key.to_paserk_id()
== "k4.lid._D6kgTzxgiPGk35gMj9bukgj4En2H94u22wVX9zaoh05"
)
assert (
private_key.to_paserk()
== "k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog"
)
assert (
public_key.to_paserk_id() == "k4.pid.yh4-bJYjOYAG6CWy0zsfPmpKylxS7uAWrxqVmBN2KAiJ"
)If you call to_paserk with wrapping_key, you can get a wrapped (encrypted) PASERK with the wrapping key.
The wrapped PASERK can be decrypted by calling from_paserk with wrapping key.
In case of local-wrap.pie:
import pyseto
from pyseto import Key
raw_key = Key.new(version=4, purpose="local", key=b"our-secret")
wrapping_key = token_bytes(32)
wpk = raw_key.to_paserk(wrapping_key=wrapping_key)
token = pyseto.encode(
raw_key, b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)
unwrapped_key = Key.from_paserk(wpk, wrapping_key=wrapping_key)
decoded = pyseto.decode(unwrapped_key, token)
# assert wpk == "k4.local-wrap.pie.TNKEwC4K1xBcgJ_GiwWAoRlQFE33HJO3oN9DHEZ05pieSCd-W7bgAL64VG9TZ_pBkuNBFHNrfOGHtnfnhYGdbz5-x3CxShhPJxg"
assert (
decoded.payload
== b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)In case of secret-wrap.pie:
import pyseto
from pyseto import Key
raw_private_key = Key.from_paserk(
"k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog"
)
wrapping_key = token_bytes(32)
wpk = raw_private_key.to_paserk(wrapping_key=wrapping_key)
unwrapped_private_key = Key.from_paserk(wpk, wrapping_key=wrapping_key)
token = pyseto.encode(
unwrapped_private_key,
b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}',
)
public_key = Key.from_paserk("k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI")
decoded = pyseto.decode(public_key, token)
# assert wpk == "k4.secret-wrap.pie.excv7V4-NaECy5hpji-tkSkMvyjsAgNxA-mGALgdjyvGNyDlTb89bJ35R1e3tILgbMpEW5WXMXzySe2T-sBz-ZAcs1j7rbD3ZWvsBTM6K5N9wWfAxbR4ppCXH_H5__9yY-kBaF2NimyAJyduhOhSmqLm6TTSucpAOakEJOXePW8"
assert (
decoded.payload
== b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)If you call to_paserk with password, you can get a wrapped (encrypted) PASERK with the password.
The wrapped PASERK can be decrypted by calling from_paserk with passwrod.
In case of local-pw:
import pyseto
from pyseto import Key
raw_key = Key.new(version=4, purpose="local", key=b"our-secret")
token = pyseto.encode(
raw_key, b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)
wpk = raw_key.to_paserk(password="our-secret")
unwrapped_key = Key.from_paserk(wpk, password="our-secret")
decoded = pyseto.decode(unwrapped_key, token)
# assert wpk == "k4.local-pw.HrCs9Pu-2LB0l7jkHB-x2gAAAAAA8AAAAAAAAgAAAAGttW0IHZjQCHJdg-Vc3tqO_GSLR4vzLl-yrKk2I-l8YHj6jWpC0lQB2Z7uzTtVyV1rd_EZQPzHdw5VOtyucP0FkCU"
assert (
decoded.payload
== b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)In case of secret-pw:
import pyseto
from pyseto import Key
raw_private_key = Key.from_paserk(
"k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog"
)
wpk = raw_private_key.to_paserk(password="our-secret")
unwrapped_private_key = Key.from_paserk(wpk, password="our-secret")
token = pyseto.encode(
unwrapped_private_key,
b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}',
)
public_key = Key.from_paserk("k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI")
decoded = pyseto.decode(public_key, token)
# assert wpk == "k4.secret-pw.MEMW4K1MaD5nWigCLyEyFAAAAAAA8AAAAAAAAgAAAAFU-tArtryNVjS2n2hCYiM11V6tOyuIog69Bjb0yNZanrLJ3afGclb3kPzQ6IhK8ob9E4QgRdEALGWCizZ0RCPFF_M95IQDfmdYKC0Er656UgKUK4UKG9JlxP4o81UwoJoZYz_D1zTlltipEa5RiNvUtNU8vLKoGSY"
assert (
decoded.payload
== b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)PySETO supports asymmetric encryption (key sealing) for all versions (v1, v2, v3 and v4).
A local key can be sealed with a PEM formatted asymmetric public key (RSA-4096 with public exponent 65537 for v1,
X25519 for v2/v4 and P-384 for v3) and unsealed with the corresponding secret key.
import pyseto
from pyseto import Key
private_key_pem = b"-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VuBCIEIFAF7jSCZHFgWvC8hUkXr55Az6Pot2g4zOAUxck0/6x8\n-----END PRIVATE KEY-----"
public_key_pem = b"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VuAyEAFv8IXsICYj0paznDK/99GyCsFOIGnfY87ayyNSIvSB4=\n-----END PUBLIC KEY-----"
raw_key = Key.new(version=4, purpose="local", key=b"our-secret")
token = pyseto.encode(
raw_key,
b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}',
)
sealed_key = raw_key.to_paserk(sealing_key=public_key_pem)
unsealed_key = Key.from_paserk(sealed_key, unsealing_key=private_key_pem)
decoded = pyseto.decode(unsealed_key, token)
assert (
decoded.payload
== b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}'
)Key searing for v1 and v3 have not been supported yet.
See Documentation.
You can run tests from the project root after cloning with:
$ toxIf you discover a security issue, please report it via a GitHub issue and avoid disclosing exploit details publicly until we respond.
This project has not been formally audited; use it according to your risk assessment.
Dependencies are scanned in CI with pip-audit (PyPA Advisory) and Trivy, licenses are checked with pip-licenses, and code is analyzed with CodeQL.
See SECURITY.md for our security policy and reporting guidance.
We welcome all kind of contributions, filing issues, suggesting new features or sending PRs.
$ uv sync --frozen
$ uv run pre-commit install[tool.uv] in pyproject.toml pins exclude-newer = "2 days", so resolution only
considers packages uploaded more than two days ago. This gives newly published
releases a verification window before we depend on them, and it applies to every
uv command automatically — no extra flags needed.
After changing dependencies, make sure uv.lock is updated:
$ uv add <package> # or: uv lock --upgradeCI fails if uv.lock does not resolve pyproject.toml. In CI, pip-audit reads
requirements exported from uv.lock on the fly, while Trivy scans uv.lock
directly with development dependencies included, so no separate
requirements.txt needs to be committed.
$ uv run pip-audit
$ uv run pip-licenses
$ trivy fs --severity HIGH,CRITICAL .