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

Installation

This project is available on PyPI and can be installed via pip:

pip install python-rigid

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")

Unsigned ULID Generation

For scenarios where you need simple unique identifiers without cryptographic signatures (e.g., internal use, non-security-critical operations, or when you need maximum compatibility with standard ULID format):

from rigid import Rigid

# Initialize Rigid (secret key not used for unsigned generation)
rigid = Rigid(secret_key=b"any-key")

# Generate a standard ULID without signature
ulid = rigid.generate_unsigned()
print(f"Unsigned ULID: {ulid}")
# Output: 01HQZX9T4R8K2M3N7P5Q6S8V9W (26 characters, no signature)

# Note: Unsigned ULIDs cannot be verified and don't support metadata
# They are standard ULIDs - globally unique, sortable, time-based

When to use unsigned ULIDs:

  • Internal system identifiers that don't require verification
  • Compatibility with systems expecting standard ULID format
  • When signature overhead is not acceptable
  • High-throughput scenarios where cryptographic operations are too expensive

When to use signed ULIDs (generate()):

  • Public-facing APIs and URLs
  • Security-critical operations
  • When tamper detection is required
  • Cross-service communication requiring integrity verification

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

generate_unsigned() -> str

Generate a standard ULID without signature or metadata support.

  • Returns: Standard ULID string (26 characters) without signature
  • Note: Does not support verification or metadata binding
  • Use cases: Internal identifiers, non-security-critical operations, standard ULID compatibility

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.3.tar.gz (10.2 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.3-py3-none-any.whl (6.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: python_rigid-0.1.3.tar.gz
  • Upload date:
  • Size: 10.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for python_rigid-0.1.3.tar.gz
Algorithm Hash digest
SHA256 9fa821d4c0b890f2161f0c69da6030de6ce4593a60233abf72c63c1710d08b80
MD5 1c9f52b1a8523a296f268a8ce36c2dde
BLAKE2b-256 7b2ab49b84f90c4c07ddf02497d672806214dec32e631ebb7434bd6f7ca361ae

See more details on using hashes here.

Provenance

The following attestation bundles were made for python_rigid-0.1.3.tar.gz:

Publisher: publish.yml on bahadrix/rigid

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

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

File metadata

  • Download URL: python_rigid-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 6.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for python_rigid-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 5c2cbf7bbee1deb76cc50b830720105f59121bfb73486c0967f8c8bd19721ce9
MD5 30f7f841edd064d5cd087179043cd262
BLAKE2b-256 08ed1bc137a539026b8fa5eb042ef4085adce9155e3d910951e00aef462ddb0c

See more details on using hashes here.

Provenance

The following attestation bundles were made for python_rigid-0.1.3-py3-none-any.whl:

Publisher: publish.yml on bahadrix/rigid

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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