Skip to main content

A Python implementation of PASETO/PASERK.

Project description

PySETO - A Python implementation of PASETO/PASERK

PyPI version PyPI - Python Version Documentation Status Github CI codecov Known Vulnerabilities

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 pyseto

PySETO 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.

Index

Installation

You can install PySETO with pip:

$ pip install pyseto

Supported PASETO Versions

PySETO supports all of PASETO versions and purposes below:

v4 v3 v2 v1
local
public

Supported PASERK Types

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

PASETO Usage

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.

Basic usage: v4.public

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"}'
)

Basic usage: v4.local

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"}'
)

Using serializer/deserializer for payload and footer

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"

Using Paseto class for handling registered claims

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 Usage

PASERK (Platform-Agnostic Serialized Keys) is an extension to PASETO that provides key-wrapping and serialization.

Serializing/Deserializing PASERK

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"

Serializing PASERK ID

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"
)

Key Wrapping

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"}'
)

Password-based Key Encryption

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"}'
)

Asymmetric Encryption

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.

API Reference

See Documentation.

Tests

You can run tests from the project root after cloning with:

$ tox

Security

If 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.

The Snyk badge reports results based on requirements.txt exported from uv.lock and reflects dependency scanning only.

See SECURITY.md for our security policy and reporting guidance.

Contributing

We welcome all kind of contributions, filing issues, suggesting new features or sending PRs.

Development environment

$ uv sync --frozen
$ uv run pre-commit install

Dependency updates

[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, re-export requirements.txt, which is what the dependency scanners read:

$ uv add <package>          # or: uv lock --upgrade
$ uv export --format requirements.txt --all-groups --locked \
    --no-hashes --no-editable --no-emit-project -o requirements.txt

CI fails if uv.lock does not resolve pyproject.toml, or if requirements.txt does not match the above export. A dependency bump that edits requirements.txt without relocking is therefore caught rather than silently scanned.

Scanning

$ uv run pip-audit
$ uv run pip-licenses
$ trivy fs --severity HIGH,CRITICAL .

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyseto-1.10.0.tar.gz (86.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyseto-1.10.0-py3-none-any.whl (33.7 kB view details)

Uploaded Python 3

File details

Details for the file pyseto-1.10.0.tar.gz.

File metadata

  • Download URL: pyseto-1.10.0.tar.gz
  • Upload date:
  • Size: 86.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for pyseto-1.10.0.tar.gz
Algorithm Hash digest
SHA256 f873cf2e7dcef150f26ba2e7cfa2ce3bc3be19aefc27cb5df31595233d67de73
MD5 98c8ed8794a60f58cabb0a0489d07355
BLAKE2b-256 cbc8a3fe511e364f389625d382a8b54f7241439d84dadaa51b67c8116e2e9bd0

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyseto-1.10.0.tar.gz:

Publisher: cd.yml on dajiaji/pyseto

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyseto-1.10.0-py3-none-any.whl.

File metadata

  • Download URL: pyseto-1.10.0-py3-none-any.whl
  • Upload date:
  • Size: 33.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for pyseto-1.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f194aad87c5af0a894f6a757c8f6e3d5c62221a3072381f40b99b9ac8bd0e415
MD5 e4ea8f78857f6ee86edccd3f67264127
BLAKE2b-256 333336e2dafbecc9751473e54d7954b954698520a021acef8fed998b4c1517f9

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyseto-1.10.0-py3-none-any.whl:

Publisher: cd.yml on dajiaji/pyseto

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page