Skip to main content

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

ASH Source-Available License (ASAL-1.0)

See the LICENSE for full terms.

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

ash_sdk-2.3.2.tar.gz (34.0 kB view details)

Uploaded Source

Built Distribution

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

ash_sdk-2.3.2-py3-none-any.whl (43.5 kB view details)

Uploaded Python 3

File details

Details for the file ash_sdk-2.3.2.tar.gz.

File metadata

  • Download URL: ash_sdk-2.3.2.tar.gz
  • Upload date:
  • Size: 34.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for ash_sdk-2.3.2.tar.gz
Algorithm Hash digest
SHA256 630d90d6b8eb9fd39c3e96267ee752c842df3f360763a55a3c7e99367bfdc526
MD5 58d7c9f06b887a73ee64d3614b7207d8
BLAKE2b-256 dea478e7e9000d60cef7e920b0fbff97150987db1362fbeb80f6a691374e2652

See more details on using hashes here.

File details

Details for the file ash_sdk-2.3.2-py3-none-any.whl.

File metadata

  • Download URL: ash_sdk-2.3.2-py3-none-any.whl
  • Upload date:
  • Size: 43.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for ash_sdk-2.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 3bd5f8a65fedbc3e090c7f749a46f8db1bded6669b62b2179627097919969371
MD5 3462c2e9be964da6e07449df74f9ecf7
BLAKE2b-256 02d177f8dda7d965e8f6fca423abe563f00ef0f75bbdee6ea84e8cc9ee6aaf65

See more details on using hashes here.

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