"""
ch15_pq_mldsa_sign.py
Vol II · Chapter 15 — ML-DSA-65 Signatures & Dual-Signature Pattern
====================================================================
Demonstrates:
  1. ML-DSA-65 key generation, signing a settlement instruction,
     and verification using liboqs-python.
  2. The dual-signature pattern: ECDSA-P256 + ML-DSA-65 on the
     same message, both required for acceptance during the
     transition period.

Requirements:
    pip install liboqs-python cryptography

liboqs-python builds liboqs from source automatically.
Run once after installation to verify:
    python3 -c "import oqs; print(oqs.get_enabled_sig_mechanisms())"
"""

import oqs
import json
from datetime import date
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes


# =============================================================================
# SECTION 1 — ML-DSA-65: key generation and settlement instruction signing
# =============================================================================

print("=" * 60)
print("SECTION 1: ML-DSA-65 Signature")
print("=" * 60)

# ── Step 1: Key generation (performed once, stored in HSM) ───────────────────
with oqs.Signature("ML-DSA-65") as signer:
    public_key  = signer.generate_keypair()
    private_key = signer.export_secret_key()

print(f"Public key size:  {len(public_key):,} bytes")   # 1,952 bytes
print(f"Private key size: {len(private_key):,} bytes")  # 4,032 bytes

# ── Step 2: Construct a settlement instruction ────────────────────────────────
instruction = {
    "instruction_id":    "INSTR-20260115-00042",
    "isin":              "FR0010242511",         # BNP Paribas equity
    "quantity":          10_000,
    "currency":          "EUR",
    "amount":            652_400.00,
    "settlement_date":   str(date(2026, 1, 17)),
    "delivering_party":  "FRBNPPARIBXXX",
    "receiving_party":   "FRSOCGENXXX",
    "place_of_settlement": "EUROCLEAR_FRANCE"
}

# Canonical serialisation: sort keys for deterministic bytes
message = json.dumps(instruction, sort_keys=True).encode("utf-8")

# ── Step 3: Sign the instruction ──────────────────────────────────────────────
with oqs.Signature("ML-DSA-65") as signer:
    signer.import_secret_key(private_key)
    signature = signer.sign(message)

print(f"Signature size:   {len(signature):,} bytes")  # 3,309 bytes

# ── Step 4: Verify (performed by the receiving CSD) ──────────────────────────
with oqs.Signature("ML-DSA-65") as verifier:
    is_valid = verifier.verify(message, signature, public_key)

print(f"Signature valid:  {is_valid}")                 # True

# Size comparison with ECDSA-256
print()
print("Size comparison:")
print(f"  ECDSA-P256  public key  :    64 bytes (uncompressed)")
print(f"  ECDSA-P256  signature   :    71 bytes (DER, max)")
print(f"  ML-DSA-65   public key  : {len(public_key):,} bytes")
print(f"  ML-DSA-65   signature   : {len(signature):,} bytes")
print(f"  Overhead factor (sig)   : ×{len(signature) / 71:.0f}")


# =============================================================================
# SECTION 2 — Dual-signature: ECDSA-256 + ML-DSA-65 on the same instruction
# =============================================================================

print()
print("=" * 60)
print("SECTION 2: Dual-Signature Pattern")
print("=" * 60)

# ── Classical key pair (existing infrastructure) ──────────────────────────────
ecdsa_private = ec.generate_private_key(ec.SECP256R1())
ecdsa_public  = ecdsa_private.public_key()

# ── Post-quantum key pair (new infrastructure) ────────────────────────────────
with oqs.Signature("ML-DSA-65") as s:
    mldsa_public  = s.generate_keypair()
    mldsa_private = s.export_secret_key()


def dual_sign(message: bytes, ecdsa_priv, mldsa_priv: bytes) -> dict:
    """Sign with both ECDSA-P256 and ML-DSA-65."""
    ecdsa_sig = ecdsa_priv.sign(message, ec.ECDSA(hashes.SHA256()))
    with oqs.Signature("ML-DSA-65") as s:
        s.import_secret_key(mldsa_priv)
        mldsa_sig = s.sign(message)
    return {
        "ecdsa_sig":  ecdsa_sig.hex(),
        "mldsa_sig":  mldsa_sig.hex(),
        "algorithms": ["ECDSA-P256-SHA256", "ML-DSA-65"],
    }


def dual_verify(message: bytes, sig_bundle: dict,
                ecdsa_pub, mldsa_pub: bytes) -> bool:
    """Verify both signatures — BOTH must be valid during the transition."""
    try:
        ecdsa_pub.verify(
            bytes.fromhex(sig_bundle["ecdsa_sig"]),
            message,
            ec.ECDSA(hashes.SHA256())
        )
        ecdsa_ok = True
    except Exception:
        ecdsa_ok = False

    with oqs.Signature("ML-DSA-65") as v:
        mldsa_ok = v.verify(
            message,
            bytes.fromhex(sig_bundle["mldsa_sig"]),
            mldsa_pub
        )

    return ecdsa_ok and mldsa_ok   # BOTH required during transition


# Sign and verify
sig_bundle = dual_sign(message, ecdsa_private, mldsa_private)
valid = dual_verify(message, sig_bundle, ecdsa_public, mldsa_public)

ecdsa_bytes = len(sig_bundle["ecdsa_sig"]) // 2
mldsa_bytes = len(sig_bundle["mldsa_sig"]) // 2

print(f"Dual signature valid:      {valid}")
print(f"ECDSA-P256 signature:    {ecdsa_bytes:>5} bytes")
print(f"ML-DSA-65  signature:  {mldsa_bytes:>7,} bytes")
print(f"Total signature bundle: {ecdsa_bytes + mldsa_bytes:>7,} bytes")
print()
print("During the transition, BOTH signatures are required.")
print("A missing or invalid ECDSA sig → REJECTED.")
print("A missing or invalid ML-DSA sig → REJECTED.")
print("This prevents downgrade attacks to the classical scheme.")
