macula_crypto_profile (macula v12.2.0)

View Source

The post-quantum crypto profile a node runs.

A realm runs one profile, and a station instance serves one profile. The profile is set in the macula application environment under crypto_profile. There is no default: the application refuses to start without exactly one known profile.

  • pq_pure: the CNSA 2.0 algorithms. ML-KEM-1024 key exchange as its DECLARED TARGET, ML-DSA-87 signatures, AES-256, no classical half.
  • pq_hybrid: SecP384r1MLKEM1024 hybrid key exchange as its DECLARED TARGET, ML-DSA-87 alone in TLS, and every other signature hybrid: ML-DSA-87 plus RSA-PSS-4096 with SHA-384, valid only if both halves verify.

⚠ A PROFILE SAYS NOTHING ABOUT THE KEY EXCHANGE. Both offer the groups of the macula-pqc crate, SecP384r1MLKEM1024 then SecP256r1MLKEM768, whatever a node's profile. Read a profile NAME as naming the policy the profile serves, never as a statement about the key exchange a connection got. The map declared a key_exchange_group until 12.0.0; see definition/1.

See plans/PLAN_POST_QUANTUM_SECURITY.md, decisions D1 to D5 and D24.

Summary

Functions

The profile set in the application environment, validated.

The algorithms of a profile. A signature is a list of algorithms: ML-DSA-87 first, then the classical half of a hybrid signature when the profile has one.

The profiles a node can run.

Check a configured value: it must be exactly one known profile. A list is refused, because one node runs one profile.

Types

definition/0

-type definition() ::
          #{tls_signature_scheme := mldsa87,
            identity_signature := [signature_algorithm(), ...],
            connect_proof_signature := [signature_algorithm(), ...]}.

profile/0

-type profile() :: pq_pure | pq_hybrid.

refusal/0

-type refusal() ::
          crypto_profile_missing |
          {crypto_profile_unknown, term()} |
          {crypto_profile_not_single, list()}.

rsa_pss_params/0

-type rsa_pss_params() ::
          #{modulus_bits := 4096,
            public_exponent := 65537,
            digest := sha384,
            mgf1_digest := sha384,
            salt_bytes := 48}.

signature_algorithm/0

-type signature_algorithm() :: mldsa87 | {rsa_pss, rsa_pss_params()}.

Functions

configured()

-spec configured() -> {ok, profile()} | {error, refusal()}.

The profile set in the application environment, validated.

definition(Other)

-spec definition(term()) -> {ok, definition()} | {error, {crypto_profile_unknown, term()}}.

The algorithms of a profile. A signature is a list of algorithms: ML-DSA-87 first, then the classical half of a hybrid signature when the profile has one.

What is actually negotiated

The QUIC NIF takes every TLS configuration from the macula-pqc crate, whatever the node's profile, and offers SecP384r1MLKEM1024 then SecP256r1MLKEM768 and nothing classical, so two nodes on this version negotiate SecP384r1MLKEM1024. ML-KEM-1024 is inside that group.

THE MAP DECLARES NO KEY EXCHANGE GROUP, on Raf's ruling of 2026-09-22. It declared one until then, and nothing read it: pq_hybrid named the group every node offers anyway, and pq_pure named mlkem1024, a pure group nothing offered, so the field claimed of one profile something that was never true. Pure ML-KEM-1024 is added if someone needs CNSA 2.0 alignment, and adding it is ADDITIVE: a further group offered beside the hybrids, which breaks nothing on the wire, since a peer that does not have it still agrees on a hybrid.

So a profile NAME names the policy the profile serves. It is never a statement about the key exchange a connection got. Key exchange is post-quantum on every link; the profile does not choose the group.

This makes no claim either way about the signature half of the map.

profiles()

-spec profiles() -> [profile(), ...].

The profiles a node can run.

validate(Profiles)

-spec validate(term()) -> {ok, profile()} | {error, refusal()}.

Check a configured value: it must be exactly one known profile. A list is refused, because one node runs one profile.