idea160-exprove-open-source/exprove/crypto.py

82 lines
2.5 KiB
Python

"""Lightweight Ed25519 signing helpers using PyNaCl.
Provides simple key generation, signing and verification utilities used by
the Attestation flow in ExProve. We keep this intentionally small to make
it easy to replace with HSM/DID integrations later.
"""
from __future__ import annotations
from typing import Tuple
import hashlib
import hmac
import secrets
try:
# Prefer PyNaCl if available for real Ed25519 signatures
from nacl.signing import SigningKey, VerifyKey
from nacl.encoding import HexEncoder
def generate_signing_key() -> SigningKey:
"""Generate a new Ed25519 signing key."""
return SigningKey.generate()
def signing_key_to_hex(sk: SigningKey) -> str:
return sk.encode(encoder=HexEncoder).decode("utf-8")
def verify_key_hex_from_signing_key(sk: SigningKey) -> str:
vk = sk.verify_key
return vk.encode(encoder=HexEncoder).decode("utf-8")
def sign_message_hex(sk: SigningKey, message: bytes) -> str:
"""Sign a message and return signature as hex string."""
sig = sk.sign(message).signature
return sig.hex()
def verify_signature_hex(verify_key_hex: str, message: bytes, sig_hex: str) -> bool:
"""Verify signature hex against message. Returns True if valid."""
vk = VerifyKey(verify_key_hex, encoder=HexEncoder)
try:
vk.verify(message, bytes.fromhex(sig_hex))
return True
except Exception:
return False
except Exception:
# Fallback: HMAC-SHA256-based signing for environments without PyNaCl.
# This is NOT cryptographically equivalent to Ed25519 and is only a
# pragmatic fallback for tests and lightweight local usage.
class _FallbackKey:
def __init__(self, key: bytes):
self._key = key
def raw(self) -> bytes:
return self._key
def generate_signing_key() -> _FallbackKey:
return _FallbackKey(secrets.token_bytes(32))
def signing_key_to_hex(sk: _FallbackKey) -> str:
return sk.raw().hex()
def verify_key_hex_from_signing_key(sk: _FallbackKey) -> str:
return signing_key_to_hex(sk)
def sign_message_hex(sk: _FallbackKey, message: bytes) -> str:
sig = hmac.new(sk.raw(), message, hashlib.sha256).digest()
return sig.hex()
def verify_signature_hex(verify_key_hex: str, message: bytes, sig_hex: str) -> bool:
key = bytes.fromhex(verify_key_hex)
expected = hmac.new(key, message, hashlib.sha256).digest()
return hmac.compare_digest(expected, bytes.fromhex(sig_hex))