#!/usr/bin/env python3
# ===========================================================================
# Copyright 2026 Afchine Madjlessi <afchine.mad@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# ===========================================================================
# gen_p11_thunks.py --- PKCS#11 v3.2 mechanism dispatch generator.
#
# Single source of truth for the mechanism table. Reading this script
# is the FIRST thing the evaluator does : every CKM_* the module
# implements MUST be listed here with its FIPS status, its target
# subsystem, its handler symbol, and its primary reference.
#
# Outputs (regenerated by `make generate`):
#
#   include/fhsm_pkcs11_mechanisms.h   CKM_* constants + extern decls
#   src/gen/fhsm_dispatch.c           dispatch table (mechanism -> handler)
#   docs/MECHANISMS.md                 human-readable reference
#
# The dispatch table is consumed by src/fhsm_pkcs11.c -- which never
# references CKM_* values directly. That decoupling lets the evaluator
# diff *this file* against the Security Policy (docs/FIPS_140_3.md §4)
# without inspecting any generated artifact.
#
# Profiles :
#   fips-strict : only FHSM_FIPS_APPROVED entries are dispatchable;
#                  legacy mechanisms are wired to dispatch_reject_fips()
#                  which returns FHSM_RV_FIPS_NOT_APPROVED at runtime.
#   interop     : every entry is dispatchable. Used only for migration
#                  from legacy systems; the audit log records every
#                  call to a non-approved mechanism.
#
# ===========================================================================

from __future__ import annotations

import argparse
import os
import sys
import textwrap
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional


# ---------------------------------------------------------------------------
# Mechanism descriptor schema
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class Mech:
    """One row of the PKCS#11 v3.2 mechanism table."""
    name: str                 # "CKM_AES_GCM"
    value: int                # 0x00001087
    family: str               # "AES" / "RSA" / "ML-KEM" / ...
    op: str                   # "encrypt", "sign", "digest", "keygen",
                              # "keypair", "wrap", "derive", "encap",
                              # "decap"
    handler: str              # C symbol of the dispatch function
    fips: str                 # "approved" | "non-approved" | "deprecated"
    key_type: Optional[str] = None      # "CKK_AES" / "CKK_RSA" / ...
    min_key_bits: Optional[int] = None
    max_key_bits: Optional[int] = None
    refs: tuple[str, ...] = field(default_factory=tuple)  # ["FIPS 197"]
    notes: str = ""


# ---------------------------------------------------------------------------
# Mechanism database --- the SINGLE SOURCE OF TRUTH.
# Values from PKCS#11 v3.2 §6 + IEEE-defined extensions.
# ---------------------------------------------------------------------------
MECHANISMS: tuple[Mech, ...] = (
    # === AES ===========================================================
    Mech("CKM_AES_KEY_GEN",        0x00001080, "AES",  "keygen",  "dispatch_aes_keygen",
         fips="approved", key_type="CKK_AES",
         min_key_bits=128, max_key_bits=256,
         refs=("FIPS 197",)),
    Mech("CKM_AES_CBC",            0x00001082, "AES",  "encrypt", "dispatch_aes_cbc",
         fips="approved", key_type="CKK_AES",
         min_key_bits=128, max_key_bits=256,
         refs=("FIPS 197", "SP 800-38A")),
    Mech("CKM_AES_CBC_PAD",        0x00001085, "AES",  "encrypt", "dispatch_aes_cbc_pad",
         fips="approved", key_type="CKK_AES",
         min_key_bits=128, max_key_bits=256,
         refs=("FIPS 197", "SP 800-38A", "RFC 5652 (PKCS#7 padding)"),
         notes="No integrity: decryption reports whether the PKCS#7 padding was "
               "valid, which is a padding oracle (Vaudenay 2002, POODLE). "
               "Inherent to the mechanism and not fixable in the module -- "
               "C_Decrypt has to return something. Prefer CKM_AES_GCM or "
               "CKM_AES_KEY_WRAP where an attacker can submit chosen "
               "ciphertexts. See R2 in docs/PKCS11_CHECK_FINDINGS.md."),
    Mech("CKM_AES_GCM",            0x00001087, "AES",  "encrypt", "dispatch_aes_gcm",
         fips="approved", key_type="CKK_AES",
         min_key_bits=128, max_key_bits=256,
         refs=("FIPS 197", "SP 800-38D"),
         notes="IV length fixed at 96 bits; tag length fixed at 128 bits in approved mode."),
    Mech("CKM_AES_CCM",            0x00001088, "AES",  "encrypt", "dispatch_aes_ccm",
         fips="approved", key_type="CKK_AES",
         min_key_bits=128, max_key_bits=256,
         refs=("FIPS 197", "SP 800-38C")),
    Mech("CKM_AES_CTR",            0x00001086, "AES",  "encrypt", "dispatch_aes_ctr",
         fips="approved", key_type="CKK_AES",
         min_key_bits=128, max_key_bits=256,
         refs=("FIPS 197", "SP 800-38A")),
    Mech("CKM_AES_KEY_WRAP",       0x00002109, "AES",  "wrap",    "dispatch_aes_kw",
         fips="approved", key_type="CKK_AES",
         min_key_bits=128, max_key_bits=256,
         refs=("FIPS 197", "SP 800-38F", "RFC 3394")),
    Mech("CKM_AES_KEY_WRAP_KWP",   0x0000210B, "AES",  "wrap",    "dispatch_aes_kwp",
         fips="approved", key_type="CKK_AES",
         min_key_bits=128, max_key_bits=256,
         refs=("FIPS 197", "SP 800-38F §6.3", "RFC 5649")),
    Mech("CKM_AES_CMAC",           0x0000108A, "AES",  "sign",    "dispatch_aes_cmac",
         fips="approved", key_type="CKK_AES",
         min_key_bits=128, max_key_bits=256,
         refs=("FIPS 197", "SP 800-38B")),

    Mech("CKM_AES_ECB",            0x00001081, "AES",  "encrypt", "dispatch_aes_ecb",
         fips="approved", key_type="CKK_AES",
         min_key_bits=128, max_key_bits=256,
         refs=("FIPS 197", "SP 800-38A"),
         notes="ECB single-block only; rejected for any input > 16 bytes."),

    # === SHA-2 =========================================================
    Mech("CKM_SHA256",             0x00000250, "SHA2", "digest",  "dispatch_sha256",
         fips="approved", refs=("FIPS 180-4",)),
    Mech("CKM_SHA384",             0x00000260, "SHA2", "digest",  "dispatch_sha384",
         fips="approved", refs=("FIPS 180-4",)),
    Mech("CKM_SHA512",             0x00000270, "SHA2", "digest",  "dispatch_sha512",
         fips="approved", refs=("FIPS 180-4",)),
    Mech("CKM_SHA224",             0x00000255, "SHA2", "digest",  "dispatch_sha224",
         fips="approved", refs=("FIPS 180-4",)),
    Mech("CKM_SHA512_224",         0x00000048, "SHA2", "digest",  "dispatch_sha512_224",
         fips="approved", refs=("FIPS 180-4",)),
    Mech("CKM_SHA512_256",         0x0000004C, "SHA2", "digest",  "dispatch_sha512_256",
         fips="approved", refs=("FIPS 180-4",)),

    # === SHA-3 / SHAKE =================================================
    Mech("CKM_SHA3_224",           0x000002B5, "SHA3", "digest",  "dispatch_sha3_224",
         fips="approved", refs=("FIPS 202",)),
    Mech("CKM_SHA3_256",           0x000002B0, "SHA3", "digest",  "dispatch_sha3_256",
         fips="approved", refs=("FIPS 202",)),
    Mech("CKM_SHA3_384",           0x000002C0, "SHA3", "digest",  "dispatch_sha3_384",
         fips="approved", refs=("FIPS 202",)),
    Mech("CKM_SHA3_512",           0x000002D0, "SHA3", "digest",  "dispatch_sha3_512",
         fips="approved", refs=("FIPS 202",)),
    # NOTE: SHAKE128/256 as fixed-output digests are NOT standard PKCS#11
    # mechanisms, and their previous values 0x2B8/0x2B9 collided with the
    # official CKM_SHA3_224_KEY_GEN (0x2B8) / SHA3-224 HMAC-general region,
    # which made C_GetMechanismInfo advertise a KEY_GEN as a digest (#125
    # TestMechFlagBehavioralConformance). De-advertised; the SHAKE XOF is only
    # exposed via SLH-DSA parameter sets and the (unused) KAT handlers remain.

    # === HMAC ==========================================================
    Mech("CKM_SHA256_HMAC",        0x00000251, "HMAC", "sign",    "dispatch_hmac_sha256",
         fips="approved", refs=("FIPS 198-1", "FIPS 180-4")),
    Mech("CKM_SHA384_HMAC",        0x00000261, "HMAC", "sign",    "dispatch_hmac_sha384",
         fips="approved", refs=("FIPS 198-1", "FIPS 180-4")),
    Mech("CKM_SHA512_HMAC",        0x00000271, "HMAC", "sign",    "dispatch_hmac_sha512",
         fips="approved", refs=("FIPS 198-1", "FIPS 180-4")),
    Mech("CKM_SHA3_256_HMAC",      0x000002B1, "HMAC", "sign",    "dispatch_hmac_sha3_256",
         fips="approved", refs=("FIPS 198-1", "FIPS 202")),
    Mech("CKM_SHA3_384_HMAC",      0x000002C1, "HMAC", "sign",    "dispatch_hmac_sha3_384",
         fips="approved", refs=("FIPS 198-1", "FIPS 202")),
    Mech("CKM_SHA3_512_HMAC",      0x000002D1, "HMAC", "sign",    "dispatch_hmac_sha3_512",
         fips="approved", refs=("FIPS 198-1", "FIPS 202")),

    # === RSA ===========================================================
    Mech("CKM_RSA_PKCS_KEY_PAIR_GEN", 0x00000000, "RSA", "keypair",  "dispatch_rsa_keypair",
         fips="approved", key_type="CKK_RSA",
         min_key_bits=2048, max_key_bits=4096,
         refs=("FIPS 186-5",)),
    Mech("CKM_RSA_PKCS_PSS",       0x0000000D, "RSA",  "sign",    "dispatch_rsa_pss",
         fips="approved", key_type="CKK_RSA",
         min_key_bits=2048, max_key_bits=4096,
         refs=("FIPS 186-5", "RFC 8017 §8.1")),
    Mech("CKM_SHA256_RSA_PKCS_PSS",0x00000043, "RSA",  "sign",    "dispatch_rsa_pss_sha256",
         fips="approved", key_type="CKK_RSA",
         min_key_bits=2048, max_key_bits=4096,
         refs=("FIPS 186-5",)),
    Mech("CKM_SHA384_RSA_PKCS_PSS",0x00000044, "RSA",  "sign",    "dispatch_rsa_pss_sha384",
         fips="approved", key_type="CKK_RSA",
         min_key_bits=2048, max_key_bits=4096,
         refs=("FIPS 186-5",)),
    Mech("CKM_SHA512_RSA_PKCS_PSS",0x00000045, "RSA",  "sign",    "dispatch_rsa_pss_sha512",
         fips="approved", key_type="CKK_RSA",
         min_key_bits=2048, max_key_bits=4096,
         refs=("FIPS 186-5",)),
    Mech("CKM_RSA_PKCS_OAEP",      0x00000009, "RSA",  "encrypt", "dispatch_rsa_oaep",
         fips="approved", key_type="CKK_RSA",
         min_key_bits=2048, max_key_bits=4096,
         refs=("RFC 8017 §7.1",)),

    Mech("CKM_RSA_PKCS",           0x00000001, "RSA",  "encrypt", "dispatch_rsa_pkcs",
         fips="non-approved", key_type="CKK_RSA",
         refs=("RFC 8017 §7.2",),
         notes="PKCS#1 v1.5 encryption is deprecated by NIST; padding-oracle risk."),
    Mech("CKM_RSA_X_509",          0x00000003, "RSA",  "encrypt", "dispatch_rsa_x509",
         fips="non-approved", key_type="CKK_RSA",
         notes="Raw RSA --- forbidden in approved mode."),
    Mech("CKM_SHA1_RSA_PKCS",      0x00000006, "RSA",  "sign",    "dispatch_sha1_rsa",
         fips="non-approved",
         notes="SHA-1 is forbidden for signature generation per SP 800-131A rev. 2."),

    # === ECDSA / ECDH ==================================================
    Mech("CKM_EC_KEY_PAIR_GEN",    0x00001040, "EC",   "keypair", "dispatch_ec_keypair",
         fips="approved", key_type="CKK_EC",
         refs=("FIPS 186-5",),
         notes="Approved curves : P-256, P-384, P-521 only."),
    Mech("CKM_ECDSA",              0x00001041, "EC",   "sign",    "dispatch_ecdsa",
         fips="approved", key_type="CKK_EC",
         refs=("FIPS 186-5",)),
    Mech("CKM_ECDSA_SHA256",       0x00001044, "EC",   "sign",    "dispatch_ecdsa_sha256",
         fips="approved", key_type="CKK_EC",
         refs=("FIPS 186-5",)),
    Mech("CKM_ECDSA_SHA384",       0x00001045, "EC",   "sign",    "dispatch_ecdsa_sha384",
         fips="approved", key_type="CKK_EC",
         refs=("FIPS 186-5",)),
    Mech("CKM_ECDSA_SHA512",       0x00001046, "EC",   "sign",    "dispatch_ecdsa_sha512",
         fips="approved", key_type="CKK_EC",
         refs=("FIPS 186-5",)),
    Mech("CKM_ECDH1_DERIVE",       0x00001050, "EC",   "derive",  "dispatch_ecdh1",
         fips="approved", key_type="CKK_EC",
         refs=("SP 800-56A rev. 3",)),
    Mech("CKM_ECDH1_COFACTOR_DERIVE", 0x00001051, "EC", "derive",  "dispatch_ecdh1_cofactor",
         fips="approved", key_type="CKK_EC",
         refs=("SP 800-56A rev. 3",)),

    # === EdDSA / X25519 / X448 =========================================
    Mech("CKM_EC_EDWARDS_KEY_PAIR_GEN", 0x00001055, "EdDSA", "keypair", "dispatch_eddsa_keypair",
         fips="approved", key_type="CKK_EC_EDWARDS",
         refs=("FIPS 186-5",),
         notes="Ed25519 (curve = 1.3.101.112) and Ed448 (curve = 1.3.101.113)."),
    Mech("CKM_EDDSA",              0x00001057, "EdDSA","sign",    "dispatch_eddsa",
         fips="approved", key_type="CKK_EC_EDWARDS",
         refs=("FIPS 186-5", "RFC 8032")),
    Mech("CKM_EC_MONTGOMERY_KEY_PAIR_GEN", 0x00001056, "ECM", "keypair", "dispatch_ecm_keypair",
         fips="approved", key_type="CKK_EC_MONTGOMERY",
         refs=("SP 800-56A rev. 3 (X25519/X448 added in rev. 3 §5.7.1)",)),
    Mech("CKM_X25519_DERIVE",      0x00001052, "ECM",  "derive",  "dispatch_x25519",
         fips="approved", key_type="CKK_EC_MONTGOMERY",
         refs=("SP 800-56A rev. 3", "RFC 7748")),
    Mech("CKM_X448_DERIVE",        0x00001054, "ECM",  "derive",  "dispatch_x448",
         fips="approved", key_type="CKK_EC_MONTGOMERY",
         refs=("SP 800-56A rev. 3", "RFC 7748")),

    # === ML-KEM (FIPS 203) ============================================
    Mech("CKM_ML_KEM_KEY_PAIR_GEN", 0x0000000f, "ML-KEM", "keypair", "dispatch_ml_kem_keypair",
         fips="approved", key_type="CKK_ML_KEM",
         refs=("FIPS 203",),
         notes="Parameter set selected via CKA_PARAMETER_SET = ML-KEM-{512,768,1024}."),
    Mech("CKM_ML_KEM",             0x00000017, "ML-KEM", "encap",   "dispatch_ml_kem_encap",
         fips="approved", key_type="CKK_ML_KEM",
         refs=("FIPS 203",)),

    # === ML-DSA (FIPS 204) ============================================
    Mech("CKM_ML_DSA_KEY_PAIR_GEN", 0x0000001c, "ML-DSA", "keypair", "dispatch_ml_dsa_keypair",
         fips="approved", key_type="CKK_ML_DSA",
         refs=("FIPS 204",),
         notes="Parameter set selected via CKA_PARAMETER_SET = ML-DSA-{44,65,87}."),
    Mech("CKM_ML_DSA",              0x0000001d, "ML-DSA","sign",    "dispatch_ml_dsa",
         fips="approved", key_type="CKK_ML_DSA",
         refs=("FIPS 204",)),

    # === SLH-DSA (FIPS 205) ===========================================
    Mech("CKM_SLH_DSA_KEY_PAIR_GEN", 0x0000002d, "SLH-DSA", "keypair", "dispatch_slh_dsa_keypair",
         fips="approved", key_type="CKK_SLH_DSA",
         refs=("FIPS 205",),
         notes="Parameter set selected via CKA_PARAMETER_SET = SLH-DSA-{SHA2,SHAKE}-{128,192,256}{s,f}."),
    Mech("CKM_SLH_DSA",             0x0000002e, "SLH-DSA","sign",   "dispatch_slh_dsa",
         fips="approved", key_type="CKK_SLH_DSA",
         refs=("FIPS 205",)),

    # === HKDF =========================================================
    Mech("CKM_HKDF_DERIVE",        0x0000402A, "HKDF", "derive",  "dispatch_hkdf",
         fips="approved", refs=("SP 800-56C rev. 2",)),
    Mech("CKM_HKDF_DATA",          0x0000402B, "HKDF", "derive",  "dispatch_hkdf_data",
         fips="approved", refs=("SP 800-56C rev. 2",)),
    Mech("CKM_HKDF_KEY_GEN",       0x0000402C, "HKDF", "keygen",  "dispatch_hkdf_keygen",
         fips="approved", refs=("SP 800-56C rev. 2",)),

    # === PBKDF2 / SP 800-132 ==========================================
    Mech("CKM_PKCS5_PBKD2",        0x000003B0, "PBKDF2","derive", "dispatch_pbkdf2",
         fips="approved",
         refs=("SP 800-132", "PKCS #5 v2.1"),
         notes="Minimum 200_000 iterations enforced by fhsm_pbkdf2()."),

    # === DRBG / RNG ===================================================
    Mech("CKM_NIST_PRF_KDF",       0x00000384, "KDF",  "derive",  "dispatch_nist_prf_kdf",
         fips="approved", refs=("SP 800-108",)),

    # === Generic secret =============================================
    Mech("CKM_GENERIC_SECRET_KEY_GEN", 0x00000350, "GENERIC", "keygen", "dispatch_generic_secret_keygen",
         fips="approved", key_type="CKK_GENERIC_SECRET",
         min_key_bits=128, max_key_bits=4096,
         refs=("SP 800-133 rev. 2",)),

    # === Concatenation KDFs (PKCS#11 v3.2 §6.20) =====================
    Mech("CKM_CONCATENATE_BASE_AND_KEY",  0x00000360, "KDF-concat", "derive",
         "dispatch_concat_base_and_key",
         fips="approved",
         refs=("PKCS#11 v3.2 §6.20.1",)),
    Mech("CKM_CONCATENATE_BASE_AND_DATA", 0x00000362, "KDF-concat", "derive",
         "dispatch_concat_base_and_data",
         fips="approved",
         refs=("PKCS#11 v3.2 §6.20.2",),
         notes="Also exposed as CKM_CONCATENATE_BASE_AND_DATA_KDF (alias)."),
    Mech("CKM_CONCATENATE_DATA_AND_BASE", 0x00000363, "KDF-concat", "derive",
         "dispatch_concat_data_and_base",
         fips="approved",
         refs=("PKCS#11 v3.2 §6.20.3",)),
    Mech("CKM_XOR_BASE_AND_DATA",         0x00000364, "KDF-concat", "derive",
         "dispatch_xor_base_and_data",
         fips="approved",
         refs=("PKCS#11 v3.2 §6.20.4",)),

    # === KMAC (SP 800-185, MAC streaming variant) =====================
    Mech("CKM_KMAC128", 0x00004080, "KMAC", "sign", "dispatch_kmac128",
         fips="approved",
         refs=("SP 800-185 §4",),
         notes="Streaming-friendly via EVP_MAC_init/update/final ; PKCS#11 v3.2 §6.22."),
    Mech("CKM_KMAC256", 0x00004081, "KMAC", "sign", "dispatch_kmac256",
         fips="approved",
         refs=("SP 800-185 §4",)),

    # === Hybrid KEM (post-quantum + classical) ========================
    Mech("CKM_HYBRID_X25519_ML_KEM_768", 0x80004200, "Hybrid-KEM",
         "encap", "dispatch_hybrid_x25519_ml_kem_768",
         fips="approved",
         refs=("SP 800-227", "draft-ietf-pquip-pqt-hybrid",
                "draft-ietf-tls-hybrid-design"),
         notes="ss = SHA3-256(ss_x25519 || ss_ml_kem_768 || ct_pqkem || ct_x25519)"),

    # === Composite ML-DSA (draft-ietf-lamps-pq-composite-sigs) =========
    #
    # Non-approved on purpose, and the reasoning belongs here rather than in a
    # commit message. Section 10.2 of the draft states the design goal that a
    # composite "be able to be considered FIPS-approved even when one of the
    # component algorithms is not" -- and, two lines earlier, that the guidance
    # "is not authoritative and has not been endorsed by US NIST". Announcing
    # it as approved would assert a status the specification itself declines to
    # assert, which is the habit this project spent 2026-08-03 removing from
    # its own documentation. Both components are individually approved
    # (FIPS 204, FIPS 186-5); the construction's standing is what is open.
    #
    # Section 10.2 also requires, for certification, that the ML-DSA seed be
    # the direct output of an approved DRBG. Ours comes from OpenSSL's RAND
    # via EVP_PKEY_keygen, not from fhsm_drbg. That gap is recorded in
    # docs/COMPOSITE_SIGS_GAP.md and would have to close before the profile
    # question could be revisited.
    Mech("CKM_COMPOSITE_MLDSA65_ED25519", 0x80004202, "Composite-Sig",
         "sign", "dispatch_composite_mldsa65_ed25519",
         fips="non-approved",
         refs=("draft-ietf-lamps-pq-composite-sigs-19",
               "id-MLDSA65-Ed25519-SHA512, OID 1.3.6.1.5.5.7.6.48"),
         notes="Composite ML-DSA as specified: both components sign the message "
               "representative M' = Prefix || Label || len(ctx) || ctx || "
               "SHA-512(M), the ML-DSA component receives the Label as its "
               "FIPS 204 context, and the output is mldsaSig || tradSig. "
               "Unlike CKM_HYBRID_ED25519_ML_DSA_65 the components are NOT "
               "separable: neither half verifies against the bare message. "
               "Interop-only -- see the comment in gen_p11_thunks.py for why "
               "it is not announced as approved."),

    # === Hybrid signature (concatenated) ==============================
    Mech("CKM_HYBRID_ED25519_ML_DSA_65", 0x80004201, "Hybrid-Sig",
         "sign", "dispatch_hybrid_ed25519_ml_dsa_65",
         fips="approved",
         refs=("RFC 9794 (PQ/T hybrid terminology)",),
         notes="A PQ/T hybrid of local design: Ed25519 and ML-DSA-65 each sign "
               "the message and the two signatures are concatenated; verify "
               "requires both. It is NOT Composite ML-DSA "
               "(draft-ietf-lamps-pq-composite-sigs): there is no signature "
               "combiner, so no Prefix/Label/ctx binding and no "
               "non-separability -- the Ed25519 half is a valid standalone "
               "signature over the same message. Not usable in X.509 or CMS, "
               "which need a registered composite OID. See "
               "docs/COMPOSITE_SIGS_GAP.md; conforming Composite ML-DSA is "
               "task zero of #112.",
         ),

    # === Legacy / non-approved (rejected in fips-strict) ==============
    Mech("CKM_MD5",                0x00000210, "MD5",  "digest",  "dispatch_md5",
         fips="non-approved",
         refs=("RFC 1321",),
         notes="Forbidden by SP 800-131A rev. 2."),
    Mech("CKM_SHA_1",              0x00000220, "SHA1", "digest",  "dispatch_sha1",
         fips="non-approved",
         refs=("FIPS 180-4",),
         notes="Disallowed for digital signature generation; legacy verify only."),
    Mech("CKM_DES_KEY_GEN",        0x00000120, "DES",  "keygen",  "dispatch_reject_fips",
         fips="non-approved",
         notes="Single-DES forbidden by SP 800-131A rev. 2."),
    Mech("CKM_DES3_KEY_GEN",       0x00000131, "TDES", "keygen",  "dispatch_des3_keygen",
         fips="non-approved",
         notes="Triple-DES disallowed for encryption after Dec 31 2023 (SP 800-131A rev. 2)."),
    Mech("CKM_DES3_CBC",           0x00000133, "TDES", "encrypt", "dispatch_des3_cbc",
         fips="non-approved"),
    Mech("CKM_RC4",                0x00000111, "RC4",  "encrypt", "dispatch_reject_fips",
         fips="non-approved"),
    Mech("CKM_DH_PKCS_KEY_PAIR_GEN", 0x00000020, "DH",  "keypair", "dispatch_reject_fips",
         fips="non-approved",
         notes="Finite-field DH < 2048 bits forbidden; modern code should use ECDH / X25519."),
    Mech("CKM_DSA_KEY_PAIR_GEN",   0x00000010, "DSA",  "keypair", "dispatch_reject_fips",
         fips="non-approved",
         notes="DSA was withdrawn by FIPS 186-5."),
)


# ---------------------------------------------------------------------------
# Sanity check : no duplicate values, every handler is named.
# ---------------------------------------------------------------------------
def _validate(mechs: tuple[Mech, ...]) -> None:
    seen_names = {}
    seen_values = {}
    for m in mechs:
        if m.name in seen_names:
            raise ValueError(f"duplicate name {m.name}")
        if m.value in seen_values and seen_values[m.value] != m.name:
            raise ValueError(
                f"duplicate value 0x{m.value:08X} : "
                f"{seen_values[m.value]} vs {m.name}")
        seen_names[m.name] = m
        seen_values[m.value] = m.name
        if not m.handler:
            raise ValueError(f"{m.name} : empty handler")
        if m.fips not in ("approved", "non-approved", "deprecated"):
            raise ValueError(f"{m.name} : bad fips status {m.fips!r}")


# ---------------------------------------------------------------------------
# Output 1 --- include/fhsm_pkcs11_mechanisms.h
# ---------------------------------------------------------------------------
def gen_header(mechs: tuple[Mech, ...], profile: str = "fips-strict") -> str:
    lines = [
        "/* Copyright 2026 Afchine Madjlessi <afchine.mad@gmail.com> */",
        "/* SPDX-License-Identifier: Apache-2.0                       */",
        "/* AUTO-GENERATED by scripts/gen_p11_thunks.py --- DO NOT EDIT. */",
        "/* Edit scripts/gen_p11_thunks.py and rerun `make generate`.   */",
        "",
        "#ifndef FHSM_PKCS11_MECHANISMS_H",
        "#define FHSM_PKCS11_MECHANISMS_H",
        "",
        "#include \"fhsm_common.h\"",
        "",
        "#ifdef __cplusplus",
        "extern \"C\" {",
        "#endif",
        "",
        "/* ---- Build profile flag ----",
        " * 1 = fips-strict (non-approved mechanisms rejected in the",
        " *     operation path) ; 0 = interop / general-purpose (non-",
        " *     approved mechanisms are executable). Consumed by the",
        " *     hand-written C_*Init operation gates in fhsm_pkcs11.c. */",
        f"#define FHSM_BUILD_FIPS_STRICT {1 if profile == 'fips-strict' else 0}",
        "",
        "/* ---- CKM_* mechanism identifiers (PKCS#11 v3.2 §6.3) ---- */",
    ]
    # Group by family for readability
    by_family: dict[str, list[Mech]] = {}
    for m in mechs:
        by_family.setdefault(m.family, []).append(m)
    for family, group in by_family.items():
        lines.append(f"/* --- {family} --- */")
        for m in group:
            lines.append(f"#define {m.name:<36s} 0x{m.value:08X}u")
        lines.append("")
    # Handler forward declarations (single signature)
    lines += [
        "/* ---- Dispatch handler signature ---- */",
        "/*",
        " * Every CKM_* points to a handler with this signature. Concrete",
        " * handlers live in src/fhsm_dispatch_*.c (or are stubs in src/gen/",
        " * fhsm_dispatch.c for unimplemented mechanisms).",
        " *",
        " *   session_handle  --- caller's PKCS#11 session",
        " *   key_handle      --- input key handle (0 if N/A for digest)",
        " *   params, plen    --- mechanism-specific parameters (e.g.",
        " *                       CK_GCM_PARAMS, CK_RSA_PKCS_OAEP_PARAMS)",
        " *   in, inlen       --- input data slice",
        " *   out, *outlen    --- output buffer (in/out length)",
        " */",
        "typedef fhsm_rv_t (*fhsm_mech_handler_t)(",
        "    unsigned long  session_handle,",
        "    unsigned long  key_handle,",
        "    const void    *params,",
        "    size_t         plen,",
        "    fhsm_slice_t   in,",
        "    uint8_t       *out,",
        "    size_t        *outlen);",
        "",
        "/* Forward declarations of every handler symbol --- ordered by family. */",
    ]
    seen_handlers = set()
    for m in mechs:
        if m.handler in seen_handlers:
            continue
        # Skip dispatch_reject_fips here : it has its own explicit
        # declaration further down. Emitting it twice triggers
        # -Werror=redundant-decls on gcc 14+.
        if m.handler == "dispatch_reject_fips":
            continue
        seen_handlers.add(m.handler)
        lines.append(f"extern fhsm_rv_t {m.handler}(unsigned long, unsigned long, "
                      f"const void*, size_t, fhsm_slice_t, uint8_t*, size_t*);")
    lines += [
        "",
        "/* ---- Dispatch table ---- */",
        "typedef struct fhsm_mech_entry_s {",
        "    uint32_t              ckm_value;",
        "    const char           *name;",
        "    const char           *family;",
        "    const char           *operation;",
        "    int                   fips_approved;   /* 0/1 */",
        "    fhsm_mech_handler_t   handler;",
        "} fhsm_mech_entry_t;",
        "",
        "extern const fhsm_mech_entry_t fhsm_mechanism_table[];",
        "extern const size_t            fhsm_mechanism_count;",
        "",
        "/* Lookup --- O(log N), table is sorted by ckm_value. Returns NULL",
        " * if the mechanism is unknown. */",
        "const fhsm_mech_entry_t *fhsm_mechanism_lookup(uint32_t ckm);",
        "",
        "/* Standard reject handler --- returns FHSM_RV_FIPS_NOT_APPROVED.",
        " * Used as the handler for every non-approved CKM in the strict",
        " * profile build. */",
        "fhsm_rv_t dispatch_reject_fips(unsigned long, unsigned long,",
        "                                const void*, size_t, fhsm_slice_t,",
        "                                uint8_t*, size_t*);",
        "",
        "#ifdef __cplusplus",
        "}",
        "#endif",
        "",
        "#endif /* FHSM_PKCS11_MECHANISMS_H */",
        "",
    ]
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Output 2 --- src/gen/fhsm_dispatch.c
# ---------------------------------------------------------------------------
def gen_dispatch(mechs: tuple[Mech, ...], profile: str) -> str:
    sorted_mechs = sorted(mechs, key=lambda m: m.value)
    # Collect unique handler symbols that are not the universal reject
    real_handlers = sorted({m.handler for m in mechs
                            if m.handler != "dispatch_reject_fips"})
    lines = [
        "/* Copyright 2026 Afchine Madjlessi <afchine.mad@gmail.com> */",
        "/* SPDX-License-Identifier: Apache-2.0                       */",
        "/* AUTO-GENERATED by scripts/gen_p11_thunks.py --- DO NOT EDIT. */",
        f"/* Profile : {profile} */",
        "",
        "#include \"fhsm_pkcs11_mechanisms.h\"",
        "#include \"fhsm_audit.h\"",
        "#include \"fhsm_mode.h\"",
        "",
        "#include <stddef.h>",
        "",
        "/* Forward decl : legacy dispatcher (weak symbol). */",
        "__attribute__((weak)) fhsm_rv_t fhsm_legacy_dispatch(",
        "    unsigned long session, unsigned long key,",
        "    const void *params, size_t plen,",
        "    fhsm_slice_t in, uint8_t *out, size_t *outlen);",
        "",
        "/* Default reject handler --- runtime mode-sensitive.",
        " *  - FIPS strict : FHSM_RV_FIPS_NOT_APPROVED",
        " *  - Legacy mode : delegate to fhsm_legacy_dispatch if present. */",
        "fhsm_rv_t dispatch_reject_fips(unsigned long session, unsigned long key,",
        "                                const void *params, size_t plen,",
        "                                fhsm_slice_t in, uint8_t *out, size_t *outlen)",
        "{",
        "    if (fhsm_mode_is_fips()) {",
        "        (void)session; (void)key; (void)params; (void)plen;",
        "        (void)in; (void)out; (void)outlen;",
        "        return FHSM_RV_FIPS_NOT_APPROVED;",
        "    }",
        "    if (fhsm_legacy_dispatch != NULL) {",
        "        return fhsm_legacy_dispatch(session, key, params, plen,",
        "                                      in, out, outlen);",
        "    }",
        "    (void)session; (void)key; (void)params; (void)plen;",
        "    (void)in; (void)out; (void)outlen;",
        "    return FHSM_RV_FIPS_NOT_APPROVED;",
        "}",
        "",
        "/* ---- Weak stubs for every approved handler -----------------",
        " *",
        " * Each handler below is declared __attribute__((weak)) so the",
        " * concrete implementation (e.g. src/dispatch/fhsm_dispatch_aes.c)",
        " * can override it at link time. While that file is missing, the",
        " * stub returns FHSM_RV_FUNCTION_FAILED so the linker still",
        " * resolves the dispatch table. This lets the project build and",
        " * the smoke tests run before every mechanism has its production",
        " * implementation in place.                                       */",
        "",
    ]
    for h in real_handlers:
        lines += [
            f"__attribute__((weak)) fhsm_rv_t {h}(",
            "    unsigned long session, unsigned long key,",
            "    const void *params, size_t plen,",
            "    fhsm_slice_t in, uint8_t *out, size_t *outlen)",
            "{",
            "    (void)session; (void)key; (void)params; (void)plen;",
            "    (void)in; (void)out; (void)outlen;",
            "    return FHSM_RV_FUNCTION_FAILED;",
            "}",
            "",
        ]
    lines.append(
        f"const int fhsm_build_fips_strict = "
        f"{1 if profile == 'fips-strict' else 0};")
    lines.append("")
    lines.append("const fhsm_mech_entry_t fhsm_mechanism_table[] = {")
    for m in sorted_mechs:
        approved = 1 if m.fips == "approved" else 0
        # In strict profile, non-approved mechanisms point at reject.
        # In interop profile, they keep their real handler (audit will
        # flag every invocation).
        handler = m.handler
        if profile == "fips-strict" and not approved:
            handler = "dispatch_reject_fips"
        lines.append(
            f"    {{ 0x{m.value:08X}u, \"{m.name}\", \"{m.family}\", "
            f"\"{m.op}\", {approved}, {handler} }},"
        )
    lines += [
        "};",
        "",
        "const size_t fhsm_mechanism_count =",
        "    sizeof(fhsm_mechanism_table) / sizeof(fhsm_mechanism_table[0]);",
        "",
        "/* Binary search over the (already-sorted) table. */",
        "const fhsm_mech_entry_t *fhsm_mechanism_lookup(uint32_t ckm)",
        "{",
        "    size_t lo = 0, hi = fhsm_mechanism_count;",
        "    while (lo < hi) {",
        "        size_t mid = (lo + hi) / 2;",
        "        uint32_t v = fhsm_mechanism_table[mid].ckm_value;",
        "        if      (v == ckm) return &fhsm_mechanism_table[mid];",
        "        else if (v <  ckm) lo = mid + 1;",
        "        else               hi = mid;",
        "    }",
        "    return NULL;",
        "}",
        "",
    ]
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Output 3 --- docs/MECHANISMS.md
# ---------------------------------------------------------------------------
def gen_doc(mechs: tuple[Mech, ...], profile: str) -> str:
    by_family: dict[str, list[Mech]] = {}
    for m in mechs:
        by_family.setdefault(m.family, []).append(m)
    lines = [
        "# FreeHSM C --- PKCS#11 v3.2 mechanism dispatch reference",
        "",
        "*Auto-generated by `scripts/gen_p11_thunks.py`. Do not edit by hand.*",
        "",
        f"**Profile generated**: `{profile}`",
        f"**Total mechanisms**: {len(mechs)}",
        f"**Approved**: {sum(1 for m in mechs if m.fips == 'approved')}",
        f"**Non-approved**: {sum(1 for m in mechs if m.fips == 'non-approved')}",
        "",
        "## Reading this table",
        "",
        "- **FIPS** column : `✅` = approved (dispatchable in fips-strict),",
        "  `❌` = non-approved (rejected with `FHSM_RV_FIPS_NOT_APPROVED`",
        "  in fips-strict, dispatchable but audited in interop).",
        "- **Op** column : the PKCS#11 operation category.",
        "- **Handler** : the C symbol invoked after argument validation.",
        "  In fips-strict, non-approved mechanisms have their handler",
        "  replaced by `dispatch_reject_fips` at code-gen time.",
        "",
    ]
    for family in sorted(by_family.keys()):
        group = by_family[family]
        lines.append(f"## {family}")
        lines.append("")
        lines.append("| Mechanism | Value | Op | FIPS | Handler | Reference |")
        lines.append("|---|---|---|---|---|---|")
        for m in group:
            mark = "✅" if m.fips == "approved" else "❌"
            refs = ", ".join(m.refs) if m.refs else "—"
            lines.append(
                f"| `{m.name}` | `0x{m.value:08X}` | {m.op} | {mark} | "
                f"`{m.handler}` | {refs} |")
            if m.notes:
                lines.append(f"| | | | | | _{m.notes}_ |")
        lines.append("")
    lines += [
        "## Cross-references",
        "",
        "- The single source of truth for this table is `scripts/gen_p11_thunks.py`.",
        "- The runtime dispatch table is generated into `src/gen/fhsm_dispatch.c`.",
        "- The public CKM_* values are generated into `include/fhsm_pkcs11_mechanisms.h`.",
        "- The Security Policy §4 (`docs/FIPS_140_3.md`) lists the same",
        "  approved algorithms grouped by family --- the evaluator can diff",
        "  the two documents to confirm conformance.",
        "",
    ]
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(
        description="Generate the PKCS#11 mechanism dispatch table.")
    p.add_argument("--profile", choices=("fips-strict", "interop"),
                    default="fips-strict",
                    help="Build profile. fips-strict rejects every "
                         "non-approved mechanism at code-gen time.")
    p.add_argument("--root", default=".",
                    help="Project root (where include/ src/ docs/ live).")
    args = p.parse_args(argv)

    _validate(MECHANISMS)
    root = Path(args.root).resolve()

    header_path   = root / "include" / "fhsm_pkcs11_mechanisms.h"
    dispatch_path = root / "src" / "gen" / "fhsm_dispatch.c"
    doc_path      = root / "docs" / "MECHANISMS.md"

    header_path.parent.mkdir(parents=True, exist_ok=True)
    dispatch_path.parent.mkdir(parents=True, exist_ok=True)
    doc_path.parent.mkdir(parents=True, exist_ok=True)

    header_path.write_text(gen_header(MECHANISMS, args.profile))
    dispatch_path.write_text(gen_dispatch(MECHANISMS, args.profile))
    doc_path.write_text(gen_doc(MECHANISMS, args.profile))

    n_app = sum(1 for m in MECHANISMS if m.fips == "approved")
    n_rej = sum(1 for m in MECHANISMS if m.fips != "approved")
    print(f"[gen_p11_thunks] profile={args.profile} "
          f"approved={n_app} non-approved={n_rej} total={len(MECHANISMS)}")
    print(f"[gen_p11_thunks] wrote {header_path}")
    print(f"[gen_p11_thunks] wrote {dispatch_path}")
    print(f"[gen_p11_thunks] wrote {doc_path}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
