Post-quantum signing SDK for Python — ML-DSA-65 (NIST FIPS 204)
Project description
fipsign-sdk
Post-quantum signing SDK for Python.
Signs and verifies any payload using ML-DSA-65 (NIST FIPS 204) — the post-quantum digital signature standard resistant to Shor's algorithm. Standardized by NIST in August 2024.
Not just for auth. Sign users, orders, documents, devices, events — any entity that needs a tamper-proof, quantum-resistant signature.
Install
pip install fipsign-sdk
For async support (httpx-based):
pip install fipsign-sdk[async]
Quick start
1. Create a free account at app.fipsign.dev — enter your email, verify the OTP code sent to your inbox.
2. In the dashboard, create a project, then create an API key inside that project. Save the key — it will not be shown again.
3. Use the key in your app:
from fipsign import PQAuth
pq = PQAuth("pqa_your_api_key")
sign() — Sign anything
The only required argument is sub — any string identifying the entity you want to sign. All other keyword arguments are stored in the payload and returned on verify. Cost: 1 token.
# Sign a user session
result = pq.sign("user_123", email="user@example.com", role="admin", expires_in_seconds=3600)
token = result.token
meta = result.meta
usage = result.usage
# Sign an order
result = pq.sign("order_456", amount=299.99, currency="USD", expires_in_seconds=300)
# Sign a document
result = pq.sign("doc_789", hash="sha256:abc...", signed_by="alice")
# Sign a device
result = pq.sign("device_iot_001", firmware="2.1.4")
# Monitor quota and token source
print(f"{usage.freeRemaining} free tokens remaining this month")
print(f"{usage.packRemaining} pack tokens remaining")
print(f"{usage.totalRemaining} total remaining")
print(f"charged from: {meta.source}") # "free" | "pack" | "free+pack"
sign() response shape
SignResult
.token PQToken
.payload str # base64 encoded payload
.signature str # ML-DSA-65 signature
.algorithm str # "ML-DSA-65"
.issuedAt int # Unix timestamp
.meta SignMeta
.algorithm str
.standard str # "NIST FIPS 204"
.quantumResistant bool
.expiresIn int # seconds
.issuedFor str # your developer account email
.projectId str
.tokenCost int # always 1
.source str # "free" | "pack" | "free+pack"
.usage SignUsage
.freeRemaining int
.packRemaining int
.totalRemaining int
.month str # e.g. "2026-05"
verify() — Verify a token
Never raises. Returns a VerifyResult with valid=False and an error message on any failure.
result = pq.verify(token)
if not result.valid:
raise PermissionError(result.error)
print(result.payload["sub"]) # "user_123"
print(result.payload["exp"]) # expiry timestamp (Unix)
print(result.payload["iat"]) # issued at timestamp (Unix)
# All custom fields passed to sign() are in payload too
revoke() — Revoke a token
Immediately and permanently invalidates a token. Future verify() calls will reject it even if the signature is valid and it hasn't expired. Cost: 1 token.
pq.revoke(token, "user logged out")
pq.revoke(token, "order cancelled")
pq.revoke(token, "suspicious activity detected")
Revoking an already-revoked token returns success without consuming an extra token — the operation is idempotent.
Note: Calling
revoke()on an already-expired token raisesPQAuthError(code="API_ERROR", status=400).
Flask middleware
from flask import Flask, g
from fipsign import PQAuth, flask_middleware
app = Flask(__name__)
pq = PQAuth("pqa_your_api_key")
auth = flask_middleware(pq)
@app.route("/login", methods=["POST"])
def login():
import base64, json
# authenticate user however you like, then:
result = pq.sign(user.id, email=user.email, role=user.role, expires_in_seconds=3600)
encoded = base64.b64encode(json.dumps(result.token.__dict__).encode()).decode()
return {"token": encoded}
@app.route("/logout", methods=["POST"])
def logout():
import base64, json
from flask import request
header = request.headers.get("Authorization", "")
if header.startswith("Bearer "):
from fipsign.types import PQToken
token = PQToken(**json.loads(base64.b64decode(header[7:]).decode()))
pq.revoke(token, "user logged out")
return {"success": True}
@app.route("/api/profile")
@auth
def profile():
return {"user": g.fipsign_user}
FastAPI middleware
from fastapi import FastAPI, Depends
from fipsign import PQAuth, fastapi_middleware
app = FastAPI()
pq = PQAuth("pqa_your_api_key")
require_auth = fastapi_middleware(pq)
@app.get("/api/profile")
def profile(user=Depends(require_auth)):
return {"sub": user["sub"], "role": user.get("role")}
Async client
from fipsign.async_client import AsyncPQAuth
async with AsyncPQAuth("pqa_your_api_key") as pq:
result = await pq.sign("user_123", role="admin", expires_in_seconds=3600)
v = await pq.verify(result.token)
print(v.valid, v.payload["sub"])
usage() — Token balance
Free tokens reset on the 1st of each month (UTC). Pack tokens never expire and accumulate across purchases. No token cost.
u = pq.usage()
# Current balance
print(f"Month: {u.current.month}")
print(f"Free: {u.current.freeRemaining} / {u.current.freeLimit}")
print(f"Used: {u.current.freeUsed} this month")
print(f"Pack: {u.current.packRemaining}")
print(f"Total: {u.current.totalRemaining}")
print(f"Account: {u.developer['email']}")
# 6-month history (always 6 entries, months with no activity show 0)
for entry in u.monthlyHistory:
print(f"{entry.month}: {entry.tokensUsed} used ({entry.fromFree} free + {entry.fromPack} pack)")
# Purchased packs
from datetime import datetime
for pack in u.packs:
date = datetime.fromtimestamp(pack.purchasedAt).strftime("%Y-%m-%d")
print(f"{pack.packType}: {pack.tokensPurchased} tokens — {date}")
webhooks — Real-time notifications
Events: token.signed · token.rejected · token.revoked · limit.warning · limit.reached
# Register
result = pq.webhooks.register(
url="https://yourapp.com/webhooks/fipsign",
events=["limit.warning", "limit.reached", "token.revoked"],
)
print(result.webhook.secret) # store this — shown only once
# Send a test event
pq.webhooks.test()
# Get current config (secret is never returned after registration)
config = pq.webhooks.get()
if config.webhook is None:
print("No webhook configured")
# Delete
pq.webhooks.delete()
Verifying incoming webhook requests
from fipsign.middleware import verify_webhook_signature
# Flask
@app.route("/webhooks/fipsign", methods=["POST"])
def webhook():
from flask import request
sig = request.headers.get("X-PQAuth-Signature", "")
if not verify_webhook_signature(request.data, sig, WEBHOOK_SECRET):
return {"error": "Invalid signature"}, 401
event = request.json
if event["event"] == "limit.warning":
print(f"Warning — {event['data']['freeRemaining']} tokens left")
return "ok", 200
# FastAPI
from fastapi import Request, HTTPException
@app.post("/webhooks/fipsign")
async def webhook(request: Request):
body = await request.body()
sig = request.headers.get("X-PQAuth-Signature", "")
if not verify_webhook_signature(body, sig, WEBHOOK_SECRET):
raise HTTPException(401, detail="Invalid signature")
event = await request.json()
return "ok"
ca — Certificate Authority
Issue and verify post-quantum certificates for devices, services, or any entity that needs a tamper-proof identity. Built on ML-DSA-65 — the same algorithm used for token signing.
Typical use case: A manufacturer of smart locks, IoT sensors, or logistics devices creates a CA root once per project from the dashboard. For each device manufactured, the system calls ca.issue() with the device's public key. The device stores its certificate. Revocation checks happen via ca.get_crl() and ca.is_cert_revoked().
Setup: Create a project in the dashboard, then click "Create CA" inside that project. A root certificate will be shown immediately after creation.
Save the root certificate now. It is shown only once and cannot be retrieved again. Store it in a secrets manager or secure file — treat it like a private key. You will need it for offline certificate verification via the JavaScript SDK.
One CA per project. Each project can have one root CA. The CA is created from the dashboard — not via API. When you call ca.issue() or other CA methods, the SDK automatically uses the CA associated with the project that owns the API key.
Device key pair generation
The Python SDK does not include a generate_key_pair() function. This is intentional: there is no production-ready Python library for ML-DSA-65 (NIST FIPS 204) at this time.
To generate an ML-DSA-65 key pair for a device, use the JavaScript SDK at provisioning time:
import { generateKeyPair } from 'fipsign-sdk'
const { publicKey, secretKey } = await generateKeyPair()
// store secretKey securely on the device — never send it to the server
// pass publicKey to ca.issue() to obtain a certificate
Then call ca.issue() from Python with the publicKey:
result = pq.ca.issue(
subject="device-serial-00123",
public_key=public_key_from_js, # base64 ML-DSA-65 public key
expires_in_seconds=365 * 24 * 60 * 60,
)
ca.issue() — Issue a certificate
Issue a certificate signed by your project's CA. Cost: 1 token.
expires_in_seconds is required and must be between 60 seconds (minimum) and 157,680,000 seconds (5 years maximum).
result = pq.ca.issue(
subject="device-serial-00123", # any identifier
public_key=device_public_key_b64, # base64 ML-DSA-65 public key
expires_in_seconds=365 * 24 * 60 * 60, # required — between 60s and 5 years
meta={"model": "lock-v2", "batch": "2026-05"}, # optional, max 10 keys
)
print(result.certificate.id) # cert_...
print(result.certificate.caId) # ca_... — the CA that signed it
print(result.certificate.expiresAt) # Unix timestamp
print(result.meta.certId) # same as certificate.id
ca.verify_cert() — Offline certificate verification
Not available in the Python SDK. No production-ready Python library for ML-DSA-65 (NIST FIPS 204) exists at this time.
Use the JavaScript SDK for offline verification:
import rootCert from './root-cert.json' assert { type: 'json' } const result = fipsign.ca.verifyCert(deviceCert, rootCert) if (!result.valid) return reject(result.error)For real-time server-side status checks, use
ca.get_cert()from Python instead.
ca.is_cert_revoked() — Check revocation offline
Check if a certificate appears in a CRL. Offline — pass the result of ca.get_crl().
crl_result = pq.ca.get_crl()
if pq.ca.is_cert_revoked(device_cert, crl_result.crl):
raise PermissionError("Device certificate has been revoked")
ca.get_crl() — Get the Certificate Revocation List
Fetch the current CRL for your project's CA. Free — no token cost.
Use get_crl() when you need to verify revocation in bulk — download the list once and check multiple certificates against it locally using is_cert_revoked(). For checking the status of a single certificate in real time (e.g. before a high-value transaction), use get_cert() instead.
result = pq.ca.get_crl()
print(f"CA: {result.subject}")
print(f"{len(result.crl)} revoked certificates")
for entry in result.crl:
from datetime import datetime
# entry.reason may be None if no reason was provided at revocation time
print(f"{entry.certId} — {datetime.fromtimestamp(entry.revokedAt).isoformat()} — {entry.reason or 'no reason'}")
ca.get_cert() — Get a certificate by ID
Retrieve a certificate and its current real-time status. Use this for single certificate checks before high-value operations. Free — no token cost.
result = pq.ca.get_cert("cert_...")
print(result.status.revoked) # bool
print(result.status.expired) # bool
print(result.status.revokedAt) # Unix timestamp or None
print(result.status.expiresAt) # Unix timestamp
ca.revoke_cert() — Revoke a certificate
Revoke a certificate immediately. Cost: 1 token.
pq.ca.revoke_cert("cert_...", "device decommissioned")
pq.ca.revoke_cert("cert_...", "device reported stolen")
Full device lifecycle example
import json
from fipsign import PQAuth
pq = PQAuth("pqa_your_api_key")
# 1. Factory: generate key pair using the JS SDK, store secretKey on device
# 2. Factory: issue a certificate for the device
result = pq.ca.issue(
subject="lock-serial-00123",
public_key=device_public_key_b64, # generated by JS SDK on device
expires_in_seconds=365 * 24 * 60 * 60,
meta={"model": "lock-v3", "batch": "2026-05"},
)
certificate = result.certificate
# store certificate on the device
# 3. At runtime: check the device is not revoked (server-side, real-time)
status = pq.ca.get_cert(certificate.id)
if status.status.revoked:
raise PermissionError("Device revoked")
# 4. At runtime: bulk revocation check (offline, from cached CRL)
crl_result = pq.ca.get_crl()
if pq.ca.is_cert_revoked(certificate, crl_result.crl):
raise PermissionError("Device revoked")
# 5. Decommission: revoke the certificate
pq.ca.revoke_cert(certificate.id, "device decommissioned")
Error handling
verify() never raises — it returns VerifyResult(valid=False, error="...") on any failure.
All other methods raise PQAuthError on failure.
from fipsign import PQAuth, PQAuthError
try:
result = pq.sign("user_123")
except PQAuthError as err:
match err.code:
case "INVALID_API_KEY": # key missing or doesn't start with pqa_
...
case "API_ERROR": # server returned an error (check err.status)
...
case "TIMEOUT": # request exceeded timeout
...
case "NETWORK_ERROR": # connection failed
...
case "MISSING_SUB": # sign() called without sub
...
print(err.code, err.message, err.status)
Token quota
Every account gets 10,000 free tokens per month, reset on the 1st (UTC). Unused free tokens do not carry over. Additional tokens are available as non-expiring packs, purchased from the dashboard.
Each of these operations costs 1 token: signing, verification, revocation, certificate issuance (ca.issue()), and certificate revocation (ca.revoke_cert()). Checking usage, fetching the public key, and all CA read operations (ca.get_crl(), ca.get_cert()) are free.
Rate limits
300 requests per minute per API key on /sign, /verify, and /revoke. On excess the API returns HTTP 429.
CA operations (ca.issue(), ca.revoke_cert()) are rate limited at 300 requests per minute per API key. Read operations (ca.get_crl(), ca.get_cert()) are not rate limited.
Token quota and rate limits are separate controls:
"Rate limit exceeded"→ back off and retry with exponential backoff"Token limit reached"→ purchase a pack from the dashboard, retrying won't help
Constructor options
pq = PQAuth(
api_key="pqa_...", # required — must start with pqa_
base_url="https://api.fipsign.dev", # optional, override for self-hosting
timeout=10, # optional, seconds (default: 10)
)
| Option | Type | Default | Description |
|---|---|---|---|
api_key |
str | — | Required. From the dashboard. Raises INVALID_API_KEY immediately if not prefixed with pqa_. |
base_url |
str | https://api.fipsign.dev |
Override for local dev or self-hosted instances. |
timeout |
float | 10 |
Request timeout in seconds. Raises TIMEOUT on exceeded. |
session |
requests.Session | — | Custom session (e.g. for proxies or custom TLS). |
Why ML-DSA-65?
JWT with RS256/ES256 and standard OAuth tokens use ECDSA or RSA — both vulnerable to Shor's algorithm running on a sufficiently powerful quantum computer. ML-DSA-65 is based on the hardness of lattice problems (Module-LWE / Module-SIS), which have no known quantum speedup. It was standardized by NIST in August 2024 as FIPS 204.
Integration tests
FIPSIGN_API_KEY=pqa_your_key \
WEBHOOK_URL=https://webhook.site/your-uuid \
WEBHOOK_SITE_TOKEN=your-uuid \
python tests/test_sdk.py
Links
- Dashboard: app.fipsign.dev
- Developer guide: fipsign.dev/guide
- API status: api.fipsign.dev/health
- JS SDK: npmjs.com/package/fipsign-sdk
- NIST FIPS 204: csrc.nist.gov/pubs/fips/204/final
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
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 fipsign_sdk-0.7.0.tar.gz.
File metadata
- Download URL: fipsign_sdk-0.7.0.tar.gz
- Upload date:
- Size: 35.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83f0b922e69314c148a9e6e538477d7375079eaf924b0f1990dcc266f4bc9a82
|
|
| MD5 |
48d745500953bb215b56903b617fe5a8
|
|
| BLAKE2b-256 |
3f39f02cc21c505c95c4608ab55257cc2713962e27620825a57e3c88fb2f63b4
|
File details
Details for the file fipsign_sdk-0.7.0-py3-none-any.whl.
File metadata
- Download URL: fipsign_sdk-0.7.0-py3-none-any.whl
- Upload date:
- Size: 25.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
33cf24a25d3fb4e550d64aa20a7276e390c3bed4d28456913865712f09d36a84
|
|
| MD5 |
e1aae9eb76b9fac874b2629b5ebd3a77
|
|
| BLAKE2b-256 |
24b2d3acb0f2b145005693f35952d5dabb82de2ed3c46f50d25f930a873eee68
|