Skip to main content

ASH - Authenticity & Stateless Hardening Protocol for Python

Project description

ASH Protocol - Python SDK

Developed by 3maem Co. | شركة عمائم

ASH (Authenticated Secure Hash) is a deterministic integrity verification protocol for web requests. This SDK provides request integrity protection, anti-replay mechanisms, and middleware for Flask, FastAPI, and Django.

Installation

# Basic installation
pip install ash-protocol

# With Flask support
pip install ash-protocol[flask]

# With FastAPI support
pip install ash-protocol[fastapi]

# With Redis support
pip install ash-protocol[redis]

# All features
pip install ash-protocol[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

ash_protocol-1.0.0.tar.gz (25.3 kB view details)

Uploaded Source

Built Distribution

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

ash_protocol-1.0.0-py3-none-any.whl (35.4 kB view details)

Uploaded Python 3

File details

Details for the file ash_protocol-1.0.0.tar.gz.

File metadata

  • Download URL: ash_protocol-1.0.0.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

Hashes for ash_protocol-1.0.0.tar.gz
Algorithm Hash digest
SHA256 87f841b88a64065c62e9545dac8a9bf7eb90a9b505dd70755ec37854b6d45e65
MD5 6ec11bd73596c312f1e639516e83891b
BLAKE2b-256 eb8e9257c5a118f724081dc6a3886be4109d6312d4d2828b833278faf8cb47f6

See more details on using hashes here.

File details

Details for the file ash_protocol-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: ash_protocol-1.0.0-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

Hashes for ash_protocol-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d4d58ea8f719792c05fef540adb13e0917b62139a6caaa24550ef695f19c549b
MD5 a686b11a2798caba234978289093df90
BLAKE2b-256 539d1dbd2d1bdd1765525ef0390475d8aae6107f1bd0d3c0ece0f891645818c1

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