Skip to main content

Universally unique lexicographically sortable identifier with cryptographical offline integrity check and meta appendix support

Project description

RigID

Cryptographically secured ULIDs with built-in integrity verification

Rigid generates universally unique lexicographically sortable identifiers (ULIDs) enhanced with HMAC signatures for tamper detection. Perfect for applications that expose database IDs publicly while maintaining security and preventing enumeration attacks.

Features

  • 🔒 HMAC-signed ULIDs - Every ID includes a cryptographic signature for integrity verification
  • 🛡️ Tamper detection - Instantly detect if an ID has been modified or forged
  • 📊 Tamper-proof metadata - Optionally bind additional context (user ID, resource type) protected by the same HMAC signature
  • Timestamp extraction - Extract creation timestamps from verified IDs
  • 🚀 Minimal dependencies - Only requires python-ulid
  • 🔐 Timing-attack resistant - Uses constant-time comparison for security
  • 📈 Sortable & time-based - Maintains all ULID benefits (lexicographic ordering)

Use Cases

  • Public API endpoints - Safely expose database IDs without enumeration risks
  • URL parameters - Secure resource identifiers in web applications
  • Distributed systems - Tamper-evident IDs across service boundaries
  • Audit trails - Verify ID authenticity in logs and records
  • Multi-tenant applications - Bind tenant context to prevent cross-tenant access

Quick Start

from rigid import Rigid

# Initialize with a secret key
rigid = Rigid(secret_key=b"your-secret-key")

# Generate a secure ULID
secure_id = rigid.generate()
print(secure_id)
# Output: 01HQZX9T4R8K2M3N7P5Q6S8V9W-A7B3F8K1

# Verify the ID integrity
is_valid, ulid_str, metadata = rigid.verify(secure_id)
print(f"Valid: {is_valid}, ULID: {ulid_str}")
# Output: Valid: True, ULID: 01HQZX9T4R8K2M3N7P5Q6S8V9W

Usage Examples

Basic ID Generation and Verification

from rigid import Rigid

# Initialize Rigid with your secret key
rigid = Rigid(secret_key=b"my-application-secret-key")

# Generate a secure ULID
secure_ulid = rigid.generate()
print(f"Generated: {secure_ulid}")

# Verify the integrity
is_valid, original_ulid, metadata = rigid.verify(secure_ulid)
if is_valid:
    print(f"✅ Valid ID: {original_ulid}")
else:
    print("❌ Invalid or tampered ID")

Binding Metadata to IDs

Bind additional context like user IDs, resource types, or tenant information. The metadata is cryptographically protected by the same HMAC signature, making it tamper-proof:

# Generate IDs with metadata
user_id = rigid.generate(metadata="user_12345")
order_id = rigid.generate(metadata="order:premium")
tenant_resource = rigid.generate(metadata="tenant_abc:document")

print(f"User ID: {user_id}")
# Output: 01HQZX9T4R8K2M3N7P5Q6S8V9W-A7B3F8K1-user_12345

# Verify and extract metadata
is_valid, ulid_str, extracted_metadata = rigid.verify(user_id)
if is_valid:
    print(f"ULID: {ulid_str}")
    print(f"Metadata: {extracted_metadata}")
    # Output: ULID: 01HQZX9T4R8K2M3N7P5Q6S8V9W
    #         Metadata: user_12345

Timestamp Extraction

import time

# Generate an ID
secure_id = rigid.generate()

# Extract timestamp (Unix timestamp in seconds)
timestamp = rigid.extract_timestamp(secure_id)
if timestamp:
    creation_time = time.ctime(timestamp)
    print(f"ID created at: {creation_time}")

# Extract full ULID object
ulid_obj = rigid.extract_ulid(secure_id)
if ulid_obj:
    print(f"ULID object: {ulid_obj}")
    print(f"Timestamp: {ulid_obj.timestamp}")
    print(f"Randomness: {ulid_obj.randomness}")

Custom Signature Length

# Use shorter signatures for space efficiency (less secure)
compact_rigid = Rigid(secret_key=b"secret", signature_length=4)

# Use longer signatures for higher security
secure_rigid = Rigid(secret_key=b"secret", signature_length=16)

compact_id = compact_rigid.generate()
secure_id = secure_rigid.generate()

print(f"Compact: {compact_id}")   # Shorter signature
print(f"Secure:  {secure_id}")    # Longer signature

Web Application Example

from rigid import Rigid
from flask import Flask, jsonify, request

app = Flask(__name__)
rigid = Rigid(secret_key=b"your-web-app-secret")

@app.route('/users/<user_id>')
def get_user(user_id):
    # Verify the user ID integrity
    is_valid, ulid_str, metadata = rigid.verify(user_id)
    
    if not is_valid:
        return jsonify({"error": "Invalid user ID"}), 400
    
    # Safe to use the verified ULID for database lookup
    user = database.get_user_by_ulid(ulid_str)
    return jsonify(user.to_dict())

@app.route('/users', methods=['POST'])
def create_user():
    # Create user in database
    user = create_new_user(request.json)
    
    # Generate secure public ID
    public_id = rigid.generate(metadata=f"user_{user.internal_id}")
    
    return jsonify({
        "public_id": public_id,
        "user": user.to_dict()
    })

Multi-Instance Compatibility

# Same secret key = compatible instances
rigid1 = Rigid(secret_key=b"shared-secret")
rigid2 = Rigid(secret_key=b"shared-secret")

# Generate with first instance
secure_id = rigid1.generate(metadata="shared_resource")

# Verify with second instance
is_valid, ulid_str, metadata = rigid2.verify(secure_id)
print(f"Cross-instance verification: {is_valid}")
# Output: Cross-instance verification: True

Error Handling

# Handle various error cases
test_cases = [
    "invalid-format",                    # Malformed
    "01HQZX9T4R8K2M3N7P5Q6S8V9W-WRONG", # Wrong signature
    "INVALID_ULID-A7B3F8K1",            # Invalid ULID
    "01HQZX9T4R-TOO-MANY-PARTS-HERE",   # Too many parts
]

for test_id in test_cases:
    is_valid, ulid_str, metadata = rigid.verify(test_id)
    print(f"ID: {test_id[:20]}... → Valid: {is_valid}")
    # All outputs: Valid: False

Security Considerations

  • Secret Key Management: Use a strong, unique secret key per application
  • Key Rotation: Consider implementing key rotation for long-lived applications
  • Signature Length: Balance between security (longer) and efficiency (shorter)
  • Timing Attacks: Built-in constant-time verification prevents timing-based attacks
  • Metadata Privacy: Be cautious about sensitive information in metadata

API Reference

Rigid(secret_key, signature_length=8)

Initialize a Rigid instance.

  • secret_key (bytes): Secret key for HMAC generation
  • signature_length (int): Number of signature bytes (default: 8)

generate(metadata=None) -> str

Generate a secure ULID with HMAC signature.

  • metadata (str, optional): Additional data to bind to the ID
  • Returns: Secure ULID string in format ULID-SIGNATURE or ULID-SIGNATURE-METADATA

verify(secure_ulid) -> tuple[bool, str | None, str | None]

Verify the integrity of a secure ULID.

  • secure_ulid (str): The secure ULID to verify
  • Returns: Tuple of (is_valid, ulid_str, metadata)

extract_ulid(secure_ulid) -> ULID | None

Extract the ULID object if valid.

  • secure_ulid (str): The secure ULID string
  • Returns: ULID object if valid, None otherwise

extract_timestamp(secure_ulid) -> float | None

Extract the timestamp from a secure ULID.

  • secure_ulid (str): The secure ULID string
  • Returns: Unix timestamp if valid, None otherwise

Requirements

  • Python 3.11+
  • python-ulid >= 3.0.0

License

This project is licensed under the MIT License.

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

python_rigid-0.1.1.tar.gz (18.9 kB view details)

Uploaded Source

Built Distribution

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

python_rigid-0.1.1-py3-none-any.whl (4.5 kB view details)

Uploaded Python 3

File details

Details for the file python_rigid-0.1.1.tar.gz.

File metadata

  • Download URL: python_rigid-0.1.1.tar.gz
  • Upload date:
  • Size: 18.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.5

File hashes

Hashes for python_rigid-0.1.1.tar.gz
Algorithm Hash digest
SHA256 4a217518bad9a0a934692fc86f0ad97004c73a0ca68409f1620a8a6d7f7bd404
MD5 f612163ddb9e0d32dc3e83f2de9d28c2
BLAKE2b-256 6334b05d367d81fc13c02df091418feb7e371e056e5903068c5fcbedc01b1db8

See more details on using hashes here.

File details

Details for the file python_rigid-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for python_rigid-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e8de82c37090e5345688131d377c362ffc86bf41dee3b19947ad2a2bc5f28b52
MD5 a6513a3014b0a51bca31fae3340f6a14
BLAKE2b-256 8b622a730488b79f5ae141a2d2ff4175698adc7e9e60cc2380d23b79dbd5e6d0

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