Skip to main content

Overview

VoicePact uses cryptographic digital signatures to ensure contract authenticity and non-repudiation. Each party’s confirmation is signed with unique cryptographic keys derived from their phone number, creating legally binding digital signatures.

Cryptographic Security

Ed25519 elliptic curve signatures for maximum security

Non-Repudiation

Parties cannot deny signing once signature is recorded

SMS Integration

Simple SMS confirmation creates cryptographic signature

Audit Trail

Complete signature history with timestamps and metadata

How It Works

1

Key Derivation

Unique signing keys are derived from the party’s phone number using PBKDF2 key derivation.
2

Signature Generation

When a party confirms via SMS/USSD, a digital signature is generated using their derived key.
3

Signature Recording

The signature is stored in the database with timestamp, IP address, and user agent.
4

Verification

Signatures can be verified at any time using the original contract data and phone number.

Signature Methods

VoicePact supports multiple signature methods:
  • sms_confirmation: Reply to SMS with YES/NO command
  • ussd_confirmation: Confirm through USSD menu
  • voice_confirmation: Verbal confirmation during call
  • api_signature: Direct API call with authentication

Creating Signatures

Via SMS Confirmation

When a party replies to a contract SMS:
SMS from +254712345678:
"YES-AG-2024-001234"
The system automatically:
  1. Parses the contract ID
  2. Derives the signing key for the phone number
  3. Generates a cryptographic signature
  4. Records the signature in the database
See crypto_service.py:65 for signature generation.

Via API

Create a signature programmatically:
import httpx

response = httpx.post(
    "https://api.voicepact.com/api/v1/signatures/create",
    json={
        "contract_id": "AG-2024-001234",
        "signer_phone": "+254712345678",
        "signature_method": "api_signature",
        "ip_address": "41.90.123.45",
        "user_agent": "VoicePact-Mobile/1.0"
    }
)

result = response.json()
print(f"Signature ID: {result['signature_id']}")
print(f"Signature Hash: {result['signature_hash']}")
print(f"Status: {result['status']}")

Signature Generation

The cryptographic process:
from app.services.crypto_service import get_crypto_service

crypto = get_crypto_service()

# Sign contract data
signature = crypto.sign_contract(
    contract_data="{contract_hash}:{contract_id}:{terms_json}",
    phone_number="+254712345678"
)

print(f"Signature: {signature}")
# Output: Base64-encoded Ed25519 signature

Signature Components

A signature includes:
  1. Contract Data: Hash of contract content
  2. Phone Number: Signer’s identifier
  3. Timestamp: When signature was created
  4. Message: Combined string that is signed
The message format:
{contract_data}:{phone_number}:{timestamp}
See crypto_service.py:65 for implementation.

Key Derivation

Signing keys are derived from phone numbers:
def _derive_signing_key(self, phone_number: str) -> bytes:
    """Derive a unique signing key from phone number"""
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=self.salt.encode('utf-8'),
        iterations=100000,
    )
    
    key_material = f"{self.master_key}:{phone_number}".encode('utf-8')
    return kdf.derive(key_material)
This ensures:
  • Each phone number has a unique key
  • Keys are deterministic (same phone = same key)
  • Keys cannot be reverse-engineered
  • Master key adds additional security layer
See crypto_service.py:246 for key derivation.

Signature Verification

Verify a signature:
crypto = get_crypto_service()

is_valid = crypto.verify_signature(
    contract_data="{contract_hash}:{contract_id}:{terms_json}",
    phone_number="+254712345678",
    signature="base64_encoded_signature"
)

if is_valid:
    print("Signature is valid")
else:
    print("Signature verification failed")

Time-Window Verification

Signatures are verified with time-window tolerance:
# Try multiple time windows to account for clock skew
for time_window in [0, 1, 2]:
    test_time = datetime.utcnow().replace(
        minute=(datetime.utcnow().minute // 10 - time_window) * 10,
        second=0,
        microsecond=0
    )
    test_message = f"{contract_data}:{phone_number}:{test_time.isoformat()}"
    
    try:
        public_key.verify(signature_bytes, test_message.encode('utf-8'))
        return True
    except:
        continue

return False
This allows for:
  • Clock synchronization differences
  • Network delays
  • Processing time variations
See crypto_service.py:78 for verification logic.

Database Model

Signatures are stored with complete audit information:
class ContractSignature(Base):
    __tablename__ = "contract_signatures"
    
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    contract_id: Mapped[str] = mapped_column(String(50), ForeignKey("contracts.id"))
    signer_phone: Mapped[str] = mapped_column(String(20), index=True)
    
    signature_method: Mapped[str] = mapped_column(String(20), default="sms_confirmation")
    signature_hash: Mapped[str] = mapped_column(String(128))
    signature_data: Mapped[Optional[dict]] = mapped_column(JSON, default=dict)
    
    status: Mapped[SignatureStatus] = mapped_column(
        Enum(SignatureStatus),
        default=SignatureStatus.PENDING
    )
    
    signed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
    
    ip_address: Mapped[Optional[str]] = mapped_column(String(45))
    user_agent: Mapped[Optional[str]] = mapped_column(String(200))
See contract.py:173 for model definition.

Signature Status

Signatures progress through states:
class SignatureStatus(str, enum.Enum):
    PENDING = "pending"      # Awaiting signature
    SIGNED = "signed"        # Successfully signed
    REJECTED = "rejected"    # Party declined
    EXPIRED = "expired"      # Signature window closed
See contract.py:58 for status enum.

SMS Confirmation Codes

Generate 6-digit confirmation codes:
crypto = get_crypto_service()

code = crypto.generate_sms_confirmation_code(
    contract_id="AG-2024-001234",
    phone_number="+254712345678"
)

print(f"Confirmation code: {code}")  # e.g., "482051"

Code Verification

is_valid = crypto.verify_sms_confirmation(
    contract_id="AG-2024-001234",
    phone_number="+254712345678",
    code="482051"
)

if is_valid:
    print("Code verified - create signature")
See crypto_service.py:101 for confirmation codes.

Contract Hash Generation

Every contract gets a unique cryptographic hash:
crypto = get_crypto_service()

contract_hash = crypto.generate_contract_hash(
    content=f"{transcript}:{terms_json}:{parties_list}:{timestamp}"
)

print(f"Contract Hash: {contract_hash}")
# Output: 64-character hexadecimal hash

Hash Algorithm

VoicePact uses BLAKE2b or SHA-256:
if settings.contract_hash_algorithm == "blake2b":
    hash_obj = hashlib.blake2b(content_bytes, digest_size=32)
else:
    hash_obj = hashlib.sha256(content_bytes)

return hash_obj.hexdigest()
See crypto_service.py:51 for hash generation.

Contract Integrity Validation

Verify contract hasn’t been tampered with:
crypto = get_crypto_service()

original_hash = contract.contract_hash
current_content = f"{contract.transcript}:{contract.terms}:{contract.parties}"

is_valid = crypto.validate_contract_integrity(
    original_hash=original_hash,
    current_content=current_content
)

if not is_valid:
    raise Exception("Contract has been tampered with!")
See crypto_service.py:238 for integrity validation.

Audit Signatures

Every contract action is signed for audit trail:
crypto = get_crypto_service()

audit_sig = crypto.create_audit_signature(
    action="contract_signed",
    contract_id="AG-2024-001234",
    actor="+254712345678",
    data={
        "signature_method": "sms_confirmation",
        "ip_address": "41.90.123.45",
        "timestamp": "2024-03-06T10:30:00Z"
    }
)

print(f"Audit Signature: {audit_sig}")
# Format: {timestamp}:{signature_hash}

Audit Verification

is_valid = crypto.verify_audit_signature(
    signature=audit_sig,
    action="contract_signed",
    contract_id="AG-2024-001234",
    actor="+254712345678",
    data=original_data
)
See crypto_service.py:130 for audit signatures.

Key Pair Generation

Generate Ed25519 key pairs:
crypto = get_crypto_service()

private_key, public_key = crypto.generate_key_pair()

print(f"Private Key: {private_key[:50]}...")
print(f"Public Key: {public_key[:50]}...")
Keys are Base64-encoded PEM format:
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()

private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

public_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

return (
    base64.b64encode(private_pem).decode('utf-8'),
    base64.b64encode(public_pem).decode('utf-8')
)
See crypto_service.py:27 for key generation.

Webhook Signatures

Secure webhook payloads with HMAC signatures:
crypto = get_crypto_service()

# Generate signature for outgoing webhook
payload = json.dumps(webhook_data)
signature = crypto.generate_webhook_signature(payload)

headers = {
    "X-VoicePact-Signature": signature,
    "Content-Type": "application/json"
}

Webhook Verification

# Verify incoming webhook
payload = await request.body()
signature = request.headers.get("X-Webhook-Signature")

is_valid = crypto.verify_webhook_signature(
    payload=payload.decode(),
    signature=signature
)

if not is_valid:
    raise HTTPException(status_code=401, detail="Invalid signature")
See crypto_service.py:162 for webhook signatures.

Session Tokens

Generate secure session tokens for USSD/API:
crypto = get_crypto_service()

token = crypto.generate_session_token(
    phone_number="+254712345678",
    session_type="ussd"
)

print(f"Session Token: {token}")
# Output: URL-safe Base64 string
See crypto_service.py:227 for session tokens.

Data Encryption

Encrypt sensitive contract data:
crypto = get_crypto_service()

# Encrypt
encrypted = crypto.encrypt_sensitive_data(
    data="Sensitive payment details",
    context="payment_info"
)

# Decrypt
decrypted = crypto.decrypt_sensitive_data(
    encrypted_data=encrypted,
    context="payment_info"
)
Encryption uses:
  • PBKDF2 key derivation
  • 100,000 iterations
  • Random salt per encryption
  • Context-specific keys
See crypto_service.py:184 for encryption.

Payment References

Generate unique payment reference codes:
crypto = get_crypto_service()

reference = crypto.generate_payment_reference(
    contract_id="AG-2024-001234",
    amount=350000.00,
    phone_number="+254712345678"
)

print(f"Payment Reference: {reference}")
# Output: 16-character uppercase hex (e.g., "A7F5C3D8E9B2F1D0")
See crypto_service.py:121 for payment references.

Contract Verification Codes

Generate human-readable verification codes:
crypto = get_crypto_service()

code = crypto.generate_contract_verification_code(
    contract_id="AG-2024-001234"
)

print(f"Verification Code: {code}")
# Output: "VC-A7F5C3D8"
See crypto_service.py:265 for verification codes.

Security Best Practices

  • Store master keys in secure environment variables
  • Use different keys for development/production
  • Rotate keys periodically
  • Never log private keys
  • Always verify signatures before trusting data
  • Check signature expiration times
  • Validate signer authority for contract
  • Log failed verification attempts
  • Sign all critical actions
  • Store complete signature metadata
  • Include IP addresses and timestamps
  • Make audit logs immutable
  • Verify all webhook signatures
  • Use HTTPS for webhook endpoints
  • Implement replay attack prevention
  • Rate limit webhook processing

Error Handling

from app.services.crypto_service import CryptographicError

try:
    signature = crypto.sign_contract(contract_data, phone_number)
except CryptographicError as e:
    logger.error(f"Signature generation failed: {e}")
    # Handle error - notify user, retry, etc.

try:
    is_valid = crypto.verify_signature(contract_data, phone_number, signature)
    if not is_valid:
        raise HTTPException(status_code=401, detail="Invalid signature")
except CryptographicError as e:
    logger.error(f"Signature verification failed: {e}")
    # Treat as invalid signature
    raise HTTPException(status_code=401, detail="Signature verification error")
Digital signatures in VoicePact:
  1. Legally Binding: Meet requirements for electronic signatures in many jurisdictions
  2. Non-Repudiation: Parties cannot deny signing
  3. Authentication: Phone number confirms identity
  4. Integrity: Any tampering is detectable
  5. Audit Trail: Complete record of all signatures
Consult local regulations for specific requirements in your jurisdiction.

Performance

Signature operations are highly optimized:
  • Key Derivation: ~100ms (cached per session)
  • Signature Generation: <10ms
  • Signature Verification: <10ms
  • Hash Generation: <1ms

Next Steps

Voice Contracts

Create contracts that require signatures

SMS Verification

Understand SMS-based signing

USSD Integration

Sign via USSD menus

Mobile Money

Cryptographically secure payments

Build docs developers (and LLMs) love