Geometric Proof System - Complete Guide¶
Version: 2.1 Last Updated: 2026-03-04 Architecture: Single-Tenant Rust
This document explains the Geometric Proof (GP) system - a challenge-response authentication mechanism used by Sawabona for ultra-robust license validation.
Note: The Geometric Proof system is tenant-agnostic and works identically in single-tenant and multi-tenant deployments. It validates licenses based on geometric transformations and HMAC signatures, independent of tenant context.
What is Geometric Proof?¶
Geometric Proof is NOT mathematically-based cryptography (like RSA or ECDSA) - it's a geometric manipulation system that generates proof vectors from geometric transformations. It provides a unique challenge-response protocol for license validation.
Core Concept¶
Client receives challenge: {seed, operations}
↓
Client derives the bootstrap secret (HKDF over device_hash:code_hash:challenge_id)
↓
Client derives WHICH FAMILY to draw from (derive_target_set, keyed to the secret)
↓
Client selects ONE figure of that family (derive_figure_index, keyed to the secret)
↓
Client folds the figure's descriptor into the final proof key (derive_proof_key)
↓
Client builds the seeded single-figure tile (secret-derived GeoParams) and applies operations
↓
Client extracts the canonical (quantized) GPV and HMACs it under the proof key
↓
Server reproduces the same selection + key + tile and verifies the proof
System Architecture¶
Core Components¶
sawabona-core/src/proof/- Core GP algorithm (Rust)tile.rs—build_tile()builds GP tiles from seed + figure collection + operationsinvariants.rs—compute_gpv()extracts 12-float Geometric Proof Vector-
proof.rs—compute_proof()/verify_proof()for HMAC-SHA256 proof over GPV -
Target Sets (
sawabona-core/src/targets/sets/) - Geometric primitives for tile generation - Greek System: 24 Greek letters with geometric properties (default)
- Zodiac System: 24 zodiac signs (12 Western + 12 Chinese animals)
-
Geometric Forms: 24 geometric shapes in one unified collection (
geometric_forms) -
API Endpoints (
sawabona-api/src/handlers/geometric_proof.rs) POST /api/v1/geometric-proof/challenge- Generate a challenge-
POST /api/v1/licenses/validate- Verify proof, bind device, issue JWT -
Challenge Engine (
sawabona-core/src/challenge/engine.rs) - Generates random operations based on configuration
- Supports weighted operation selection
-
Configurable challenge parameters (target set, ops range, TTL)
-
GPV Dual-Proof (
sawabona-api/src/handlers/geometric_proof.rs) - When
require_gpv = trueon a product andgpv.enabled = trueglobally, both HMAC and GPV proofs must pass - GPV proof is sent as
gpv_prooffield (base64-encoded) in the validation request - Server independently rebuilds tile, computes GPV, and verifies via constant-time comparison
How It Works¶
Geometric primitives - Three target set families available:
- Greek: 24 Greek letters (α, β, γ, δ, ... ω) — default
- Zodiac: 24 signs (12 Western: Aries...Pisces + 12 Chinese: Rat...Pig)
- Geometric Forms: 24 geometric shapes in a single unified collection (
geometric_forms) - Circle, Square, Triangle, Rectangle, Pentagon, Hexagon, Octagon, 3 Ellipse variants, Heptagon, Nonagon, Decagon, Dodecagon, 3 Stars, Rhombus, Trapezoid, Parallelogram, Cross, Arrow, wide ellipse, tilted ellipse
Geometric transformations applied to tiles:
- Rotation (45°, 90°, 135°, 180°, etc.)
- Horizontal/vertical mirroring
- Translation (X, Y, Z axis)
- Scaling (optional)
- Shearing (optional)
Invariant extraction - Geometric properties that remain constant:
- Center of mass
- Perimeter
- Angle histogram
- Stroke order parity
HMAC-based proof - Cryptographic signing of extracted invariants
Security Model¶
Security comes from:
- ✅ HMAC secret (derived from the license key via HKDF) — the sole cryptographic anchor. The algorithm and the geometric primitives are public (this is open-source software); their secrecy is not a security source. The HMAC secret is what an attacker lacks.
- ✅ Secret-derived key material (defense-in-depth) — the selected
primitive is chosen via
derive_figure_index, its canonical descriptor is folded into the proof key viaderive_proof_key, and the figure's geometry is secret-parameterized viaderive_geo_params+build_tile. So an attacker who knows the target set but not the license key cannot reconstruct the GPV. This layers additional secret-keyed material on top of the HMAC secret; it is not hard-math cryptography (no factoring or discrete-log hardness) and does not rely on algorithm obscurity. - ✅ Single-use challenges — a challenge is consumed on the first verification attempt, regardless of whether that attempt succeeds. A failed proof cannot be replayed against the same challenge until its TTL expires; a fresh challenge must be requested for each attempt.
- ✅ Challenge expiration — challenges expire after TTL (default: 300
seconds; configurable via
ttl_seconds).
Usage Examples¶
1. Generate a Challenge¶
Request:
curl -X POST http://localhost:8000/api/v1/geometric-proof/challenge \
-H "Content-Type: application/json" \
-d '{
"license_key": "default-tenant_saw_prod_live_a1b2c3d4e5f6a7b8c9d0_f0e1",
"product_slug": "prod",
"device_hash": "sha256-device-fingerprint"
}'
Response:
{
"challenge_id": "chal_a1b2c3d4e5f6g7h8",
"ttl": 300,
"seed": 4827361,
"ops": [
{ "type": "rotate", "angle": 90 },
{ "type": "mirror_h" },
{ "type": "translate", "dx": 1, "dy": -1, "dz": 0 }
],
"server_sig": "ed25519-signature-base64"
}
2. Compute Proof (Client-Side, Rust)¶
use sawabona_core::crypto::{sha256_hex, derive_key, hmac_sha256};
// 1. Derive client secret
let code_hash = sha256_hex(b"geometric_proof_core");
let info = format!("{}:{}:{}", device_hash, code_hash, challenge_id);
let client_secret = derive_key(license_key.as_bytes(), info.as_bytes(), 32)?;
// 2. Build proof data
let mut proof_data = Vec::new();
proof_data.extend_from_slice(&seed.to_le_bytes());
proof_data.extend_from_slice(challenge_id.as_bytes());
for op in &ops {
let op_sig = hmac_sha256(&client_secret, serde_json::to_string(op)?.as_bytes());
proof_data.extend_from_slice(&op_sig);
}
// 3. Final HMAC proof
let proof = hmac_sha256(&client_secret, &proof_data);
let proof_b64 = base64::encode(&proof);
3. Validate License with Proof¶
Request:
curl -X POST http://localhost:8000/api/v1/licenses/validate \
-H "Content-Type: application/json" \
-d '{
"license_key": "default-tenant_saw_prod_live_a1b2c3d4e5f6a7b8c9d0_f0e1",
"proof": {
"challenge_id": "chal_a1b2c3d4e5f6g7h8",
"proof": "base64-encoded-hmac-proof"
},
"device_fingerprint": "sha256-device-fingerprint",
"device_name": "My Workstation",
"device_type": "desktop"
}'
Response:
{
"is_valid": true,
"license_id": "uuid",
"token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"message": "License valid"
}
Configuration¶
Built-in Defaults¶
The runtime ships with ChallengeConfig::production_default() (defined in
sawabona-core/src/challenge/config.rs) as the single source of truth.
The bundled YAML at sawabona-core/config/challenges/default.yaml mirrors
those defaults, line for line:
version: "1.0"
challenge_type: "geometric"
# Informational. The family a challenge draws from is derived from the
# bootstrap secret, per challenge; this setting does not pin it.
target_set: "greek"
parameters:
max_ops: 5
min_ops: 2
ttl_seconds: 300
seed_range: [1000000, 9999999]
proof_version: 1
operations:
- type: "rotate"
enabled: true
weight: 30
params:
angles: [90, 180, 270]
- type: "mirror_h"
enabled: true
weight: 20
- type: "mirror_v"
enabled: true
weight: 20
- type: "translate"
enabled: true
weight: 15
params:
dx_range: [-2, 2]
dy_range: [-2, 2]
dz_range: [0, 0]
- type: "scale"
enabled: true
weight: 15
params:
scale_factors: [0.5, 1.0, 2.0]
Loading a Custom YAML¶
To override the built-in defaults, point the runtime at a YAML file via
your TOML config (e.g. config/development.toml):
Loading is hard-fail: a missing or invalid YAML aborts startup rather
than silently falling back. See sawabona-core/config/challenges/default.yaml
for the complete shipped reference and advanced.yaml for a higher-friction
profile (8 max ops, 60s TTL, 10M seed space, scale + shear enabled).
Storage Backend¶
Pending challenges are persisted in a backend selected by
Config.challenge.storage. The choice is operational, not cryptographic
— see sawabona-core/README.md#challenge-storage-backend for the full
matrix:
| Backend | When to use |
|---|---|
memory (default) |
Single-pod self-host. A background task purges expired entries every cleanup_interval_secs (default 540s = 9 min). |
redis |
Required for ≥2 pods or rolling restarts. TTL handled natively by Redis. |
[challenge.storage]
backend = "redis"
url = "redis://redis-host:6379"
key_prefix = "sawabona:challenge" # optional
Environment Variables¶
The Environment::with_prefix("SAWABONA") source maps env vars to TOML
keys with __ as the path separator. Useful overrides:
# Path to a challenge YAML override (maps to [challenge].config_path)
export SAWABONA_CHALLENGE__CONFIG_PATH=/etc/sawabona/challenge.yaml
# Background cleanup interval, seconds; 0 disables the task
export SAWABONA_CHALLENGE__CLEANUP_INTERVAL_SECS=60
# Global GPV kill-switch (per-product require_gpv still applies on top)
export SAWABONA_GPV__ENABLED=true
Storage backend selection is structural (tagged enum) and is best set in TOML rather than env vars.
Target Sets¶
Greek System (default)¶
24 Greek letters with geometric properties:
- Vowels: α, ε, η, ι, ο, υ, ω
- Consonants: β, γ, δ, ζ, θ, κ, λ, μ, ν, ξ, π, ρ, σ, τ, φ, χ, ψ
Each letter has geometric path definition, transformation rules, and invariant properties.
Default target set - Recommended for most use cases.
Zodiac System¶
24 zodiac signs (12 Western + 12 Chinese):
- Western (12): Aries, Taurus, Gemini, Cancer, Leo, Virgo, Libra, Scorpio, Sagittarius, Capricorn, Aquarius, Pisces
- Chinese (12): Rat, Ox, Tiger, Rabbit, Dragon, Snake, Horse, Goat, Monkey, Rooster, Dog, Pig
Each sign has SVG-like path definition, segment definitions, and transformation rules.
Alternative target set - Same cardinality as Greek (24 primitives).
Geometric Forms System¶
24 geometric shapes in a single unified geometric_forms target set, organized internally into 7 categories:
| Index | Figures | Category |
|---|---|---|
| 0–3 | Circle, Square, Triangle, Rectangle | BasicShapes |
| 4–6 | Pentagon, Hexagon, Octagon | Polygons |
| 7–9 | Ellipse (horizontal, vertical, narrow) | AdvancedShapes |
| 10–13 | Heptagon, Nonagon, Decagon, Dodecagon | ExtendedPolygons |
| 14–16 | Star-5, Star-6, Star-8 | Stars |
| 17–19 | Rhombus, Trapezoid, Parallelogram | Quadrilaterals |
| 20–23 | Cross, Arrow, wide ellipse, tilted ellipse | CompositeForms |
geometric_forms is one of three families of 24. The other two are greek —
the 24 letters of the alphabet — and zodiac — the 12 signs and the 12
planetary and alchemical symbols.
Nobody chooses which. The family is derived from the bootstrap secret, per challenge, the same way the figure index is. It is not in the challenge, not in the response, and not a parameter any client passes:
family = derive_target_set(bootstrap_secret, challenge_id) # 1 of 3
index = derive_figure_index(bootstrap_secret, challenge_id, 24)
Until 2026-08-25 the family was named in the challenge and sent to the client, so an observer reading the traffic knew which collection to look at and the unknown was one figure in 24. It is one in 72 now, and the observer learns neither half. All three families must hold the same number of figures: the index is taken over the collection, so a smaller family would shrink the unknown for every challenge that drew it.
Security Considerations¶
Strengths¶
- Unique Challenge-Response Protocol
- Different from standard JWT/OAuth flows
- Resistant to replay: a challenge is consumed on the first verification attempt regardless of success, so a captured proof cannot be re-submitted
-
Binds license validation to geometric computation
-
Client-Side Computation
- Offloads work to client (reduces server load)
- Proves the client holds the license-derived HMAC secret (anti-piracy) — the algorithm itself is public, so possessing it is not the gate
-
Reduces network traffic
-
Deterministic Invariants
- Same seed + operations always produce same GPV
- Enables reliable verification
-
No randomness in computation
-
Primitive-dependent proof
- The selected figure and its descriptor are bound into the proof key, and the figure's geometry is secret-parameterized, coupling the HMAC layer to the geometry and raising the bar for offline forgery attempts
Limitations¶
- Not Based on Hard Mathematical Problems
- RSA: Based on factoring large numbers (NP-hard)
- ECDSA: Based on discrete logarithm problem (NP-hard)
-
GP: Based on geometric transformations (not proven hard)
-
Security Depends on the HMAC Secret
- The HMAC secret (derived from the license key) is the primary security
anchor. On top of it, the choice of primitive is secret-derived
(
derive_figure_index), the primitive's canonical descriptor is folded into the proof key (derive_proof_key), and the geometry itself is secret-parameterized (derive_geo_params+build_tile), so an attacker who knows the target set but not the license key cannot reconstruct the GPV. None of these mechanisms are based on hard mathematical problems (factoring, discrete log); they provide defense-in-depth and anti-reverse-engineering hardening, not cryptographic hardness. Algorithm obscurity is not a security feature.
Best Practices¶
- Always use HTTPS - Protect challenge and proof in transit
- Validate device fingerprints - Prevent device spoofing
- Enforce challenge expiration - Prevent replay attacks
- Rotate HMAC secrets - Periodically update license keys
- Monitor for attacks - Track failed proof attempts
- Use strong license keys - Ensure sufficient entropy in HMAC secrets
Protocol Version¶
There is a single canonical protocol, carried end-to-end in a
proof_version field that is always 1:
- secret-driven figure selection (
derive_figure_index), - the selected primitive's descriptor folded into the proof key
(
derive_proof_key), - secret-parameterized seeded geometry (
derive_geo_params+build_tile), - the canonical, 6-decimal-quantized GPV (
compute_gpv) for cross-platform byte stability.
The proof_version field is retained as a forward-evolution hook so the
protocol can change later without ambiguity, but today exactly one value is
valid.
How proof_version flows¶
ChallengeConfig.parameters.proof_version (config / YAML, validated == 1)
↓ generate_challenge()
Challenge.proof_version (stamped on the stored challenge)
↓ challenge endpoint
ChallengeResponse.proof_version (returned to the client)
↓ client compute
LicenseClient::_compute_proof (rejects anything but 1)
↓ prove endpoint
verify_proof (server-authoritative gate: == 1)
Server-authoritative¶
proof_version is read from the stored challenge, never from the client's
proof submission. A client cannot alter the version it was issued. Both
verify_proof and validate_license reject any stored version other than 1
with a 400 and a clear "please upgrade your client" message before any
cryptographic work; ChallengeConfig::validate likewise rejects a non-1
proof_version at startup.
Upgrade path¶
To roll a future protocol version:
- Implement the new behavior behind a new arm in both the client
(
_compute_proof) and the server (verify_proof/validate_license), and widen the accepted-version checks. - Bump
proof_versioninChallengeParameters(config / YAML) and the config validator. - Newly generated challenges carry the new version; clients that cannot
compute it receive a
400with the upgrade message rather than a silent proof mismatch.
Implementation Details (Rust)¶
Tile Building¶
// sawabona-core/src/proof/tile.rs
pub fn build_tile(
seed: u64,
collection: &dyn FigureCollection,
ops: &[serde_json::Value],
figure_index: usize,
params: &GeoParams,
) -> Result<GeoProofTile>
// 1. Select the SINGLE figure at `figure_index`, where
// figure_index = derive_figure_index(bootstrap_secret, challenge_id, collection.count()).
// 2. Emit its segments via Figure::segments_seeded(params) — secret-derived
// sampling count, phase, vertex jitter and stroke order.
// 3. Apply operations (rotate, mirror, translate) sequentially.
// 4. Return tile with transformed segments.
Invariant Extraction¶
// sawabona-core/src/proof/invariants.rs
pub fn compute_gpv(segments: &[Segment]) -> GeometricProofVector
// Returns 12-float vector: center_x, center_y, perimeter, area,
// 6 angle histogram bins, stroke_parity, segment_count_norm
Proof Computation¶
// sawabona-core/src/proof/proof.rs
pub fn compute_proof(gpv: &GeometricProofVector, challenge_id: &str, client_secret: &[u8]) -> Result<Vec<u8>>
pub fn verify_proof(gpv: &GeometricProofVector, challenge_id: &str, client_secret: &[u8], proof: &[u8]) -> Result<bool>
// Uses HMAC-SHA256 with constant-time comparison
Troubleshooting¶
Challenge Generation Fails¶
Symptom: POST /api/v1/geometric-proof/challenge returns 400 error
Solutions:
- Verify license key is valid and active (not revoked/expired)
- Check
product_slugmatches the license's bound product - Ensure target set is configured (greek, zodiac, or geometric_forms)
- Check challenge configuration is loaded
Proof Verification Fails¶
Symptom: POST /api/v1/licenses/validate returns 401 or proof mismatch
Solutions:
- Verify challenge hasn't expired (check TTL, default 300s)
- Verify HMAC secret derivation:
HKDF(license_key, info=device_hash:code_hash:challenge_id) - Verify operations are signed in the correct order
- Check device_fingerprint matches the one used for the challenge
- If
require_gpv = true, ensuregpv_prooffield is included
Target Set Not Found¶
Symptom: "Target set not registered"
Solutions:
- Check
SAWABONA_TARGET_SETenvironment variable - Verify target set name:
greek,zodiac,geometric_forms,basic_shapes, etc. target_setin the configuration file is informational only — the family a challenge draws from is derived from the secret and cannot be pinned
References¶
- API Reference: See
reference/API-REFERENCE.mdfor endpoint details - How It Works: See
GEOMETRIC-PROOF-EXPLAINED.mdfor worked examples with actual math - Greek Target Set: See
targets/GREEK-TARGETS.mdfor Greek alphabet system details (default) - Zodiac Target Set: See
targets/ZODIAC-TARGETS.mdfor zodiac system details (alternative) - Proof System: See
sawabona-core/PROOF_SYSTEM.mdfor Rust implementation details - Testing: the protocol's own tests are in the repository —
sawabona-core/tests/gpv_dual_proof_tests.rs,sawabona-core/tests/challenge_engine_tests.rsandsawabona-core/tests/e2e_gpv_proof_test.rs. They are what an independent implementation checks itself against.