| |
| """Ed25519 SIGIL signing for SOV33 API endpoints. |
| |
| Usage: |
| from sigil_ed25519 import SigilSigner |
| |
| signer = SigilSigner() |
| sigil = signer.sign({"action": "test", "timestamp": "2026-07-26"}) |
| assert signer.verify(sigil) |
| """ |
| import json |
| import hashlib |
| import time |
| from pathlib import Path |
|
|
| try: |
| from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey |
| from cryptography.hazmat.primitives import serialization |
| HAS_CRYPTO = True |
| except ImportError: |
| HAS_CRYPTO = False |
|
|
|
|
| class SigilSigner: |
| """Ed25519 SIGIL signer for sovereign actions.""" |
| |
| def __init__(self, key_path=None): |
| if not HAS_CRYPTO: |
| raise ImportError("pip install cryptography") |
| |
| if key_path and Path(key_path).exists(): |
| self._load_key(key_path) |
| else: |
| self._generate_key(key_path) |
| |
| def _generate_key(self, save_path=None): |
| """Generate new Ed25519 key pair.""" |
| self.private_key = Ed25519PrivateKey.generate() |
| self.public_key = self.private_key.public_key() |
| |
| if save_path: |
| Path(save_path).parent.mkdir(parents=True, exist_ok=True) |
| |
| priv_pem = self.private_key.private_bytes( |
| encoding=serialization.Encoding.PEM, |
| format=serialization.PrivateFormat.PKCS8, |
| encryption_algorithm=serialization.NoEncryption() |
| ) |
| Path(save_path).write_bytes(priv_pem) |
| Path(save_path).chmod(0o600) |
| |
| |
| pub_path = str(save_path) + ".pub" |
| pub_pem = self.public_key.public_bytes( |
| encoding=serialization.Encoding.PEM, |
| format=serialization.PublicFormat.SubjectPublicKeyInfo |
| ) |
| Path(pub_path).write_bytes(pub_pem) |
| |
| def _load_key(self, key_path): |
| """Load existing Ed25519 key pair.""" |
| priv_pem = Path(key_path).read_bytes() |
| self.private_key = serialization.load_pem_private_key(priv_pem, password=None) |
| self.public_key = self.private_key.public_key() |
| |
| def sign(self, payload): |
| """Create SIGIL signature for payload.""" |
| if isinstance(payload, dict): |
| payload = json.dumps(payload, sort_keys=True).encode() |
| elif isinstance(payload, str): |
| payload = payload.encode() |
| |
| signature = self.private_key.sign(payload) |
| |
| return { |
| "payload": payload.decode() if isinstance(payload, bytes) else payload, |
| "signature": signature.hex(), |
| "algorithm": "Ed25519", |
| "timestamp": time.time(), |
| "sha256": hashlib.sha256(payload).hexdigest() |
| } |
| |
| def verify(self, sigil): |
| """Verify SIGIL signature.""" |
| try: |
| payload = sigil["payload"] |
| if isinstance(payload, str): |
| payload = payload.encode() |
| |
| signature = bytes.fromhex(sigil["signature"]) |
| self.public_key.verify(signature, payload) |
| |
| |
| expected_hash = hashlib.sha256(payload).hexdigest() |
| if sigil.get("sha256") != expected_hash: |
| return False |
| |
| return True |
| except Exception: |
| return False |
| |
| def get_public_key_hex(self): |
| """Get public key as hex string.""" |
| pub_bytes = self.public_key.public_bytes( |
| encoding=serialization.Encoding.Raw, |
| format=serialization.PublicFormat.Raw |
| ) |
| return pub_bytes.hex() |
|
|
|
|
| class SigilChain: |
| """Hash-chained SIGIL ledger.""" |
| |
| def __init__(self, signer): |
| self.signer = signer |
| self.chain = [] |
| self.prev_hash = "genesis" |
| |
| def append(self, payload): |
| """Append new SIGIL to chain.""" |
| chain_payload = { |
| "payload": payload, |
| "prev_hash": self.prev_hash, |
| "chain_index": len(self.chain), |
| } |
| |
| sigil = self.signer.sign(chain_payload) |
| sigil["chain_hash"] = hashlib.sha256( |
| (self.prev_hash + sigil["sha256"]).encode() |
| ).hexdigest() |
| |
| self.chain.append(sigil) |
| self.prev_hash = sigil["chain_hash"] |
| |
| return sigil |
| |
| def verify_chain(self): |
| """Verify entire chain integrity.""" |
| prev_hash = "genesis" |
| |
| for i, sigil in enumerate(self.chain): |
| |
| if not self.signer.verify(sigil): |
| return False, f"Signature invalid at index {i}" |
| |
| |
| expected_hash = hashlib.sha256( |
| (prev_hash + sigil["sha256"]).encode() |
| ).hexdigest() |
| if sigil.get("chain_hash") != expected_hash: |
| return False, f"Chain broken at index {i}" |
| |
| prev_hash = sigil["chain_hash"] |
| |
| return True, "Chain valid" |
|
|
|
|
| |
| _signer = None |
|
|
| def get_signer(key_path="/workspace/sigil_key.pem"): |
| """Get or create global signer.""" |
| global _signer |
| if _signer is None: |
| _signer = SigilSigner(key_path) |
| return _signer |
|
|
|
|
| def sign_response(response_data): |
| """Sign API response with SIGIL.""" |
| signer = get_signer() |
| return signer.sign(response_data) |
|
|
|
|
| def verify_sigil(sigil): |
| """Verify SIGIL signature.""" |
| signer = get_signer() |
| return signer.verify(sigil) |
|
|
|
|
| if __name__ == "__main__": |
| |
| signer = SigilSigner("/tmp/sigil_demo_key.pem") |
| |
| |
| sigil = signer.sign({"action": "test", "data": "hello world"}) |
| print("Signed:", json.dumps(sigil, indent=2)[:200]) |
| |
| |
| valid = signer.verify(sigil) |
| print("Valid:", valid) |
| |
| |
| chain = SigilChain(signer) |
| chain.append({"step": 1, "data": "first"}) |
| chain.append({"step": 2, "data": "second"}) |
| chain.append({"step": 3, "data": "third"}) |
| |
| valid, msg = chain.verify_chain() |
| print(f"Chain: {len(chain.chain)} entries, valid={valid}, msg={msg}") |
|
|