ASH SDK for Python - Request integrity and anti-replay protection library
Project description
ASH SDK for Python
Developed by 3maem Co. | شركة عمائم
ASH SDK provides request integrity and anti-replay protection for web applications. This SDK provides request integrity protection, anti-replay mechanisms, and middleware for Flask, FastAPI, and Django.
Installation
# Basic installation
pip install ash-sdk
# With Flask support
pip install ash-sdk[flask]
# With FastAPI support
pip install ash-sdk[fastapi]
# With Redis support
pip install ash-sdk[redis]
# All features
pip install ash-sdk[all]
Requirements: Python 3.10 or later
Quick Start
Canonicalize JSON
from ash.canonicalize import ash_canonicalize_json
# Canonicalize JSON to deterministic form
canonical = ash_canonicalize_json('{"z":1,"a":2}')
print(canonical) # {"a":2,"z":1}
Build a Proof
from ash.proof import ash_build_proof
from ash.canonicalize import ash_canonicalize_json
from ash.core import AshMode
# Canonicalize payload
payload = '{"username":"test","action":"login"}'
canonical = ash_canonicalize_json(payload)
# Build proof
proof = ash_build_proof(
mode=AshMode.BALANCED,
binding="POST /api/login",
context_id="ctx_abc123",
nonce=None, # Optional: for server-assisted mode
canonical_payload=canonical
)
print(f"Proof: {proof}")
Verify a Proof
from ash.compare import ash_timing_safe_equal
expected_proof = "abc123..."
received_proof = "abc123..."
# Use timing-safe comparison to prevent timing attacks
if ash_timing_safe_equal(expected_proof, received_proof):
print("Proof verified successfully")
else:
print("Proof verification failed")
Flask Integration
from flask import Flask, jsonify, request
from ash.stores import MemoryStore
from ash.server import context
from ash.middleware.flask import ash_flask_middleware
import asyncio
app = Flask(__name__)
store = MemoryStore()
# Issue context endpoint
@app.route("/ash/context", methods=["POST"])
def get_context():
ctx = asyncio.run(context.create(
store,
binding="POST /api/update",
ttl_ms=30000,
))
return jsonify({
"contextId": ctx.context_id,
"expiresAt": ctx.expires_at,
"mode": ctx.mode.value,
})
# Protected endpoint
@app.route("/api/update", methods=["POST"])
@ash_flask_middleware(store, expected_binding="POST /api/update")
def update():
# Request verified - safe to process
return jsonify({"status": "success"})
if __name__ == "__main__":
app.run()
FastAPI Integration
from fastapi import FastAPI, Depends
from ash.stores import MemoryStore
from ash.server import context
from ash.middleware.fastapi import AshMiddleware, ash_verify
app = FastAPI()
store = MemoryStore()
# Add ASH middleware
app.add_middleware(AshMiddleware, store=store, protected_paths=["/api/*"])
# Issue context endpoint
@app.post("/ash/context")
async def get_context():
ctx = await context.create(
store,
binding="POST /api/update",
ttl_ms=30000,
)
return {
"contextId": ctx.context_id,
"expiresAt": ctx.expires_at,
"mode": ctx.mode.value,
}
# Protected endpoint
@app.post("/api/update")
async def update():
# Request verified by middleware
return {"status": "success"}
Django Integration
# settings.py
MIDDLEWARE = [
# ...
'ash.middleware.django.AshMiddleware',
]
ASH_SETTINGS = {
'STORE': 'ash.stores.RedisStore',
'REDIS_URL': 'redis://localhost:6379/0',
'PROTECTED_PATHS': ['/api/*'],
}
# views.py
from django.http import JsonResponse
from ash.server import context
async def get_context(request):
ctx = await context.create(
request.ash_store,
binding="POST /api/update",
ttl_ms=30000,
)
return JsonResponse({
"contextId": ctx.context_id,
"expiresAt": ctx.expires_at,
"mode": ctx.mode.value,
})
API Reference
Canonicalization
ash_canonicalize_json(input_json: str) -> str
Canonicalizes JSON to deterministic form.
Rules:
- Object keys sorted lexicographically
- No whitespace
- Unicode NFC normalized
from ash.canonicalize import ash_canonicalize_json
canonical = ash_canonicalize_json('{"z":1,"a":2}')
# Result: '{"a":2,"z":1}'
ash_canonicalize_urlencoded(input_data: str) -> str
Canonicalizes URL-encoded data.
from ash.canonicalize import ash_canonicalize_urlencoded
canonical = ash_canonicalize_urlencoded('z=1&a=2')
# Result: 'a=2&z=1'
Proof Generation
ash_build_proof(mode, binding, context_id, nonce, canonical_payload) -> str
Builds a cryptographic proof.
from ash.proof import ash_build_proof
from ash.core import AshMode
proof = ash_build_proof(
mode=AshMode.BALANCED,
binding="POST /api/update",
context_id="ctx_abc123",
nonce=None, # Optional
canonical_payload='{"name":"John"}'
)
ash_verify_proof(expected: str, actual: str) -> bool
Verifies two proofs match using constant-time comparison.
from ash.proof import ash_verify_proof
is_valid = ash_verify_proof(expected_proof, received_proof)
Binding Normalization
ash_normalize_binding(method: str, path: str) -> str
Normalizes a binding string to canonical form.
Rules:
- Method uppercased
- Path starts with /
- Query string excluded
- Duplicate slashes collapsed
- Trailing slash removed (except for root)
from ash.binding import ash_normalize_binding
binding = ash_normalize_binding("post", "/api//test/")
# Result: 'POST /api/test'
Secure Comparison
ash_timing_safe_equal(a: str | bytes, b: str | bytes) -> bool
Performs constant-time comparison to prevent timing attacks.
from ash.compare import ash_timing_safe_equal
is_equal = ash_timing_safe_equal("secret1", "secret2")
Security Modes
from ash.core import AshMode
class AshMode(Enum):
MINIMAL = "minimal" # Basic integrity checking
BALANCED = "balanced" # Recommended for most applications
STRICT = "strict" # Maximum security with nonce requirement
| Mode | Description |
|---|---|
MINIMAL |
Basic integrity checking |
BALANCED |
Recommended for most applications |
STRICT |
Maximum security with server nonce |
Context Stores
MemoryStore
In-memory store for development and testing.
from ash.stores import MemoryStore
store = MemoryStore()
RedisStore
Production-ready store with atomic operations.
import redis
from ash.stores import RedisStore
redis_client = redis.Redis(host='localhost', port=6379, db=0)
store = RedisStore(redis_client)
Client Usage
For Python clients making requests to ASH-protected endpoints:
import requests
from ash.canonicalize import ash_canonicalize_json
from ash.proof import ash_build_proof
from ash.core import AshMode
import json
# 1. Get context from server
ctx_response = requests.post("https://api.example.com/ash/context").json()
# 2. Prepare payload
payload = {"name": "John", "action": "update"}
payload_json = json.dumps(payload)
canonical = ash_canonicalize_json(payload_json)
# 3. Build proof
proof = ash_build_proof(
mode=AshMode(ctx_response["mode"]),
binding="POST /api/update",
context_id=ctx_response["contextId"],
nonce=ctx_response.get("nonce"),
canonical_payload=canonical
)
# 4. Make protected request
response = requests.post(
"https://api.example.com/api/update",
json=payload,
headers={
"X-ASH-Context-ID": ctx_response["contextId"],
"X-ASH-Proof": proof,
}
)
Using the Client Helper
from ash.client import AshClient
import requests
client = AshClient()
# Get context from server
ctx_response = requests.post("https://api.example.com/ash/context").json()
# Build proof headers automatically
headers = client.build_headers(
context_id=ctx_response["contextId"],
mode=ctx_response["mode"],
binding="POST /api/update",
payload={"name": "John"},
nonce=ctx_response.get("nonce"),
)
# Make protected request
response = requests.post(
"https://api.example.com/api/update",
json={"name": "John"},
headers=headers,
)
Complete Server Example
from flask import Flask, jsonify, request
from ash.stores import RedisStore
from ash.server import context, verify
from ash.canonicalize import ash_canonicalize_json
from ash.proof import ash_build_proof
from ash.core import AshMode
import redis
import asyncio
app = Flask(__name__)
# Production Redis store
redis_client = redis.Redis(host='localhost', port=6379, db=0)
store = RedisStore(redis_client)
@app.route("/ash/context", methods=["POST"])
def issue_context():
"""Issue a new ASH context."""
binding = request.json.get("binding", "POST /api/update")
ctx = asyncio.run(context.create(
store,
binding=binding,
ttl_ms=30000,
mode=AshMode.BALANCED,
))
return jsonify({
"contextId": ctx.context_id,
"expiresAt": ctx.expires_at,
"mode": ctx.mode.value,
})
@app.route("/api/update", methods=["POST"])
def update():
"""Protected endpoint with manual verification."""
context_id = request.headers.get("X-ASH-Context-ID")
proof = request.headers.get("X-ASH-Proof")
if not context_id or not proof:
return jsonify({"error": "Missing ASH headers"}), 403
# Verify the request
result = asyncio.run(verify.verify_request(
store=store,
context_id=context_id,
proof=proof,
binding="POST /api/update",
payload=request.get_data(as_text=True),
content_type=request.content_type,
))
if not result.valid:
return jsonify({
"error": result.error_code.value,
"message": result.error_message,
}), 403
# Request verified - safe to process
return jsonify({"status": "success"})
if __name__ == "__main__":
app.run(debug=True)
Error Handling
from ash.core import AshErrorCode
class AshErrorCode(Enum):
INVALID_CONTEXT = "ASH_INVALID_CONTEXT"
CONTEXT_EXPIRED = "ASH_CONTEXT_EXPIRED"
REPLAY_DETECTED = "ASH_REPLAY_DETECTED"
INTEGRITY_FAILED = "ASH_INTEGRITY_FAILED"
ENDPOINT_MISMATCH = "ASH_ENDPOINT_MISMATCH"
CANONICALIZATION_FAILED = "ASH_CANONICALIZATION_FAILED"
Type Hints
The SDK is fully typed for IDE support:
from ash.core import AshMode, AshContext, AshVerifyResult
def process_context(ctx: AshContext) -> None:
print(f"Context ID: {ctx.context_id}")
print(f"Expires at: {ctx.expires_at}")
print(f"Mode: {ctx.mode}")
License
MIT License
Links
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 ash_sdk-1.0.1.tar.gz.
File metadata
- Download URL: ash_sdk-1.0.1.tar.gz
- Upload date:
- Size: 25.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d8a06b9771a5a5c03f4eef2483afe40397b24fe2b03398cc0c7724f46450e3a2
|
|
| MD5 |
2647909f9ba4730079d6619b5d50f260
|
|
| BLAKE2b-256 |
33e6ba0df3eb08ff5465610b7dc77d748fcfb37cd8f2f6e59d1ace92961f31ee
|
File details
Details for the file ash_sdk-1.0.1-py3-none-any.whl.
File metadata
- Download URL: ash_sdk-1.0.1-py3-none-any.whl
- Upload date:
- Size: 35.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2be1008c48790b24671672314a1c9825a50f15097805af3babfbf311e6e0f2d6
|
|
| MD5 |
35535d530c2e6a2c9e7563bcd6bafeb8
|
|
| BLAKE2b-256 |
23b422361d98a013fec928e024c0807de78a8784334ff5011c3fcfc58b798a7c
|