From 40cfb8bc301e7f744d5bdeb9dc14c4e762b961e6 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Wed, 10 Dec 2025 07:56:48 +0100 Subject: [PATCH 01/21] Add PQSignature keys nist_level property. --- pq_logic/keys/abstract_pq.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pq_logic/keys/abstract_pq.py b/pq_logic/keys/abstract_pq.py index bdd93700..01a53551 100644 --- a/pq_logic/keys/abstract_pq.py +++ b/pq_logic/keys/abstract_pq.py @@ -88,6 +88,11 @@ def sig_size(self) -> int: """Return the size of the signature.""" return self._sig_method.details["length_signature"] + @property + def nist_level(self) -> int: + """Return the claimed NIST security level as string.""" + return int(self._sig_method.details["claimed_nist_level"]) + class PQSignaturePrivateKey(PQPrivateKey, ABC): """Abstract base class for Post-Quantum Signature Private Keys.""" @@ -162,6 +167,11 @@ def from_private_bytes(cls, data: bytes, name: str) -> "PQSignaturePrivateKey": raise ValueError(f"Invalid key size expected {key.key_size}, but got: {len(data)}") return key + @property + def nist_level(self) -> int: + """Return the claimed NIST security level as string.""" + return self.public_key().nist_level + class PQKEMPublicKey(PQPublicKey, KEMPublicKey, ABC): """Abstract base class for Post-Quantum KEM Public Keys.""" From c19f74ed756d0a277f4203aeafc883e9a7a94806 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Wed, 10 Dec 2025 07:57:24 +0100 Subject: [PATCH 02/21] Add PQHashStatefulSig keys key_bit_security property. --- pq_logic/keys/abstract_stateful_hash_sig.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pq_logic/keys/abstract_stateful_hash_sig.py b/pq_logic/keys/abstract_stateful_hash_sig.py index a63ab54d..e81b2946 100644 --- a/pq_logic/keys/abstract_stateful_hash_sig.py +++ b/pq_logic/keys/abstract_stateful_hash_sig.py @@ -73,6 +73,11 @@ def max_sig_size(self) -> int: def hash_alg(self) -> str: """Return the hash algorithm used by this public key.""" + @property + @abstractmethod + def key_bit_security(self) -> int: + """Return the estimated security strength in bits.""" + class PQHashStatefulSigPrivateKey(PQPrivateKey, ABC): """Abstract base class for Post-Quantum Hash Stateful Signature Private Keys.""" @@ -131,3 +136,8 @@ def used_keys(self) -> list[bytes]: def hash_alg(self) -> str: """Return the hash algorithm used by this private key.""" return self.public_key().hash_alg + + @property + def key_bit_security(self) -> int: + """Return the estimated security strength in bits.""" + return self.public_key().key_bit_security From ba2299d36164f1b4aa59be58c54dda39c62d785b Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Wed, 10 Dec 2025 07:59:23 +0100 Subject: [PATCH 03/21] Implement nist_level property for ML-DSA and SLH-DSA keys. --- pq_logic/keys/sig_keys.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pq_logic/keys/sig_keys.py b/pq_logic/keys/sig_keys.py index 8cbd2719..fcbcb4bb 100644 --- a/pq_logic/keys/sig_keys.py +++ b/pq_logic/keys/sig_keys.py @@ -187,6 +187,12 @@ def from_public_bytes(cls, data: bytes, name: str) -> "MLDSAPublicKey": ) return key + @property + def nist_level(self) -> int: + """Return the NIST security level of the ML-DSA algorithm.""" + nist_levels = {"ml-dsa-44": 2, "ml-dsa-65": 3, "ml-dsa-87": 5} + return nist_levels[self.name] + ML_DSA_PRIVATE_KEY_SIZE = {"ml-dsa-44": 2560, "ml-dsa-65": 4032, "ml-dsa-87": 4896} @@ -562,6 +568,19 @@ def from_public_bytes(cls, data: bytes, name: str) -> "SLHDSAPublicKey": raise ValueError(f"Invalid public key size. Expected: {_n}, got: {len(data)}") return key + @property + def nist_level(self) -> int: + """Return the nist level.""" + if oqs is not None: + return super().nist_level + if self.name.endswith("128s") or self.name.endswith("128f"): + return 1 + if self.name.endswith("192s") or self.name.endswith("192f"): + return 3 + if self.name.endswith("256s") or self.name.endswith("256f"): + return 5 + raise ValueError(f"Cannot determine NIST level for SLH-DSA algorithm: {self.name}") + class SLHDSAPrivateKey(PQSignaturePrivateKey): """Represents an SLH-DSA private key.""" From ce3907846b9390f2636d56025d5af7606cf94c8b Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Wed, 10 Dec 2025 07:59:51 +0100 Subject: [PATCH 04/21] Implement key_bit_security for HSS, XMSS and XMSS^MT keys. --- pq_logic/keys/stateful_sig_keys.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pq_logic/keys/stateful_sig_keys.py b/pq_logic/keys/stateful_sig_keys.py index 816efed0..bf251992 100644 --- a/pq_logic/keys/stateful_sig_keys.py +++ b/pq_logic/keys/stateful_sig_keys.py @@ -508,6 +508,12 @@ def hash_alg(self) -> str: return "shake128" return XMSS_ALG_DETAILS[self.name.lower()]["hash_alg"] + @property + def key_bit_security(self) -> int: + """Return the traditional bit security of the XMSS public key.""" + # Approximate bit security based on the hash output size + return int(self.name.split("_")[-1]) // 2 + class XMSSPrivateKey(PQHashStatefulSigPrivateKey): """Class representing an XMSS private key.""" @@ -772,6 +778,12 @@ def hash_alg(self) -> str: """Return the hash algorithm used by this XMSSMT public key.""" return XMSSMT_ALG_DETAILS[self.name.lower()]["hash_alg"] + @property + def key_bit_security(self) -> int: + """Return the pq bit security of the XMSSMT public key.""" + # Approximate bit security based on the hash output size + return XMSSMT_ALG_DETAILS[self.name.lower()]["n"] * 4 + class XMSSMTPrivateKey(PQHashStatefulSigPrivateKey): """Class representing an XMSSMT private key.""" @@ -1085,6 +1097,15 @@ def get_level_names(self) -> Sequence[Tuple[str, str]]: levels = [(lms_name, lmots_name)] * num_levels return levels + @property + def key_bit_security(self) -> int: + """Return the key bit security of this HSS key.""" + # TODO fix for multiple levels with different parameters. + n = self._details["n"] + if n == 24: + return 96 + return 128 + class HSSPrivateKey(PQHashStatefulSigPrivateKey): """Representation of an HSS private key.""" From d963607803b08b9c286df948fdd3ad2c7bdcc2a7 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Wed, 10 Dec 2025 08:01:32 +0100 Subject: [PATCH 05/21] Update ALL_KNOWN_OIDS_2_NAME to update with OID_HASH_MAP. --- resources/oidutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/oidutils.py b/resources/oidutils.py index 782d6d5a..bb400fc2 100644 --- a/resources/oidutils.py +++ b/resources/oidutils.py @@ -479,6 +479,7 @@ ALL_KNOWN_OIDS_2_NAME.update({rfc6664.id_ecPublicKey: "ecPublicKey"}) ALL_KNOWN_OIDS_2_NAME.update(RFC9481_OID_2_NAME) ALL_KNOWN_OIDS_2_NAME.update(HMAC_NAME_2_OID) +ALL_KNOWN_OIDS_2_NAME.update(OID_HASH_MAP) ########################### From 220fd9432b78856c18e9629a20d25f15953fa0e6 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Sat, 17 Jan 2026 13:11:17 +0100 Subject: [PATCH 06/21] Add security_utils with NIST level to strength mapping --- resources/security_utils.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 resources/security_utils.py diff --git a/resources/security_utils.py b/resources/security_utils.py new file mode 100644 index 00000000..b251e9d0 --- /dev/null +++ b/resources/security_utils.py @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: Copyright 2025 Siemens AG +# +# SPDX-License-Identifier: Apache-2.0 +# + +"""Contains security related utility functions, like getting the bit string of a used key.""" + + +# Security strength values follow NIST SP 800-57 Part 1 Revision 5, Tables 2 and 4. +# Table 2 provides the traditional key equivalence for RSA/DSA and ECC key sizes, +# while Table 4 lists the target security strengths for the NIST PQC levels. +_NIST_LEVEL_TO_STRENGTH = { + 1: 128, + 2: 192, + 3: 192, + 4: 256, + 5: 256, +} \ No newline at end of file From 6ab92feb5db8f893b462117db195c90441989aa2 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Sat, 17 Jan 2026 13:12:09 +0100 Subject: [PATCH 07/21] Add HASH_ALG_TO_STRENGTH mapping in security_utils --- resources/security_utils.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/resources/security_utils.py b/resources/security_utils.py index b251e9d0..0ef7b76c 100644 --- a/resources/security_utils.py +++ b/resources/security_utils.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # -"""Contains security related utility functions, like getting the bit string of a used key.""" +"""Contain security-related utility functions, like getting the bit string of a used key.""" # Security strength values follow NIST SP 800-57 Part 1 Revision 5, Tables 2 and 4. @@ -15,4 +15,18 @@ 3: 192, 4: 256, 5: 256, +} + +HASH_ALG_TO_STRENGTH = { + "sha1": 80, + "sha224": 112, + "sha256": 128, + "sha384": 192, + "sha512": 256, + "sha3_224": 112, + "sha3_256": 128, + "sha3_384": 192, + "sha3_512": 256, + "shake128": 128, # Uses in CMP 32 Byte as output size. According to RFC 9481. + "shake256": 256, # Uses in CMP 64 Byte as output size. According to RFC 9481. } \ No newline at end of file From 921b8aea11b1b9a07d72a5c0f0b86f465c0c45d3 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Sat, 17 Jan 2026 13:12:38 +0100 Subject: [PATCH 08/21] Add _rsa_security_strength function to map RSA key sizes to security strength levels --- resources/security_utils.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/resources/security_utils.py b/resources/security_utils.py index 0ef7b76c..51aba799 100644 --- a/resources/security_utils.py +++ b/resources/security_utils.py @@ -5,7 +5,6 @@ """Contain security-related utility functions, like getting the bit string of a used key.""" - # Security strength values follow NIST SP 800-57 Part 1 Revision 5, Tables 2 and 4. # Table 2 provides the traditional key equivalence for RSA/DSA and ECC key sizes, # while Table 4 lists the target security strengths for the NIST PQC levels. @@ -29,4 +28,30 @@ "sha3_512": 256, "shake128": 128, # Uses in CMP 32 Byte as output size. According to RFC 9481. "shake256": 256, # Uses in CMP 64 Byte as output size. According to RFC 9481. -} \ No newline at end of file +} + + +def _rsa_security_strength(key_size: int) -> int: + """Return an approximate security strength (in bits) for an RSA key size. + + Mapping follows NIST SP 800-57 Part 1 Rev. 5 Table 2. + """ + if key_size < 1024: + return 64 + + if key_size <= 1024: + return 80 + + if key_size <= 2048: + return 112 + + if key_size <= 3072: + return 128 + + if key_size <= 7680: + return 192 + + if key_size <= 15360: + return 256 + + return 256 From 574434534399c52220ad7138c6bac13f0f62ab7f Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Sat, 17 Jan 2026 13:12:58 +0100 Subject: [PATCH 09/21] Add _ecc_security_strength function to map ECC key sizes to security strength levels --- resources/security_utils.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/resources/security_utils.py b/resources/security_utils.py index 51aba799..31fb7904 100644 --- a/resources/security_utils.py +++ b/resources/security_utils.py @@ -55,3 +55,29 @@ def _rsa_security_strength(key_size: int) -> int: return 256 return 256 + + +def _ecc_security_strength(key_size: int) -> int: + """Return the security strength (in bits) for an ECC-style curve size. + + Mapping follows NIST SP 800-57 Part 1 Rev. 5 Table 2. + """ + # Table 2 (ECC column: f is the field size in bits): + # - f = 160–223 -> strength 80 + # - f = 224–255 -> strength 112 + # - f = 256–383 -> strength 128 + # - f = 384–511 -> strength 192 + # - f = 512+ -> strength 256 + if key_size <= 223: + return 80 + + if key_size <= 255: + return 112 + + if key_size <= 383: + return 128 + + if key_size <= 511: + return 192 + + return 256 From 79e911a375cd54c1924ff29db05471f05e8cc145 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Sat, 17 Jan 2026 13:13:43 +0100 Subject: [PATCH 10/21] Add _get_pq_stfl_nist_security_strength and _nist_level_strength utility functions for PQ keys --- resources/security_utils.py | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/resources/security_utils.py b/resources/security_utils.py index 31fb7904..ccf08170 100644 --- a/resources/security_utils.py +++ b/resources/security_utils.py @@ -5,6 +5,11 @@ """Contain security-related utility functions, like getting the bit string of a used key.""" +from typing import Optional + +from pq_logic.keys.abstract_stateful_hash_sig import PQHashStatefulSigPublicKey +from pq_logic.keys.stateful_sig_keys import HSSPublicKey, XMSSMTPublicKey, XMSSPublicKey + # Security strength values follow NIST SP 800-57 Part 1 Revision 5, Tables 2 and 4. # Table 2 provides the traditional key equivalence for RSA/DSA and ECC key sizes, # while Table 4 lists the target security strengths for the NIST PQC levels. @@ -81,3 +86,37 @@ def _ecc_security_strength(key_size: int) -> int: return 192 return 256 + + +def _get_pq_stfl_nist_security_strength(key: PQHashStatefulSigPublicKey) -> int: + """Return the PQ security strength (in bits) for a PQ stateful signature key. + + XMSS and XMSS^MT security strength is determined by the hash function output size. + According to RFC 8391 Section 5. Parameter Sets. + The security strength is halved when considering PQ security strength, because of the `Grover` algorithm. + + :param key: The PQ stateful signature public key. + :return: The security strength in bits. + :raises NotImplementedError: If the key type is not supported. + """ + if isinstance(key, XMSSPublicKey): + return key.key_bit_security + if isinstance(key, XMSSMTPublicKey): + return key.key_bit_security + if isinstance(key, HSSPublicKey): + return key.key_bit_security + + raise NotImplementedError( + f"Security strength estimation is only implemented for XMSS and XMSSMT keys. Got: {type(key)}" + ) + + +def _nist_level_strength(level: Optional[int]) -> int: + """Translate a claimed NIST level into an approximate security strength. + + Mapping follows NIST SP 800-57 Part 1 Rev. 5 Table 4. + """ + if level is None: + return 0 + + return _NIST_LEVEL_TO_STRENGTH.get(int(level), 0) From 222b1833733d9da52491f31621bb2fb67e143c48 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Sat, 17 Jan 2026 13:15:08 +0100 Subject: [PATCH 11/21] Implement `estimate_key_security_strength` to calculate security strength for various key types --- resources/security_utils.py | 72 ++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/resources/security_utils.py b/resources/security_utils.py index ccf08170..717088e9 100644 --- a/resources/security_utils.py +++ b/resources/security_utils.py @@ -5,10 +5,16 @@ """Contain security-related utility functions, like getting the bit string of a used key.""" -from typing import Optional +from typing import Optional, Union +from cryptography.hazmat.primitives.asymmetric import ed448, x448, ed25519, x25519, dsa, rsa +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey + +from pq_logic.keys.abstract_pq import PQKEMPublicKey, PQSignaturePublicKey from pq_logic.keys.abstract_stateful_hash_sig import PQHashStatefulSigPublicKey +from pq_logic.keys.abstract_wrapper_keys import HybridPublicKey, TradKEMPublicKey from pq_logic.keys.stateful_sig_keys import HSSPublicKey, XMSSMTPublicKey, XMSSPublicKey +from resources.typingutils import PrivateKey, PublicKey # Security strength values follow NIST SP 800-57 Part 1 Revision 5, Tables 2 and 4. # Table 2 provides the traditional key equivalence for RSA/DSA and ECC key sizes, @@ -120,3 +126,67 @@ def _nist_level_strength(level: Optional[int]) -> int: return 0 return _NIST_LEVEL_TO_STRENGTH.get(int(level), 0) + + +def estimate_key_security_strength(key: Union[PrivateKey, PublicKey]) -> int: + """Estimate the security strength of a key in bits. + + :param key: The key to estimate the security strength for. + :return: The estimated security strength in bits. + :raises NotImplementedError: If the key type is not supported for security strength estimation. + """ + if isinstance(key, PrivateKey): + key = key.public_key() + + if isinstance(key, PQHashStatefulSigPublicKey): + return _get_pq_stfl_nist_security_strength(key) + + if isinstance( + key, + ( + PQKEMPublicKey, + PQSignaturePublicKey, + ), + ): + return _nist_level_strength(key.nist_level) + + if hasattr(key, "nist_level"): + return _nist_level_strength(getattr(key, "nist_level")) + + if isinstance(key, rsa.RSAPublicKey): + return _rsa_security_strength(key.key_size) + + if isinstance(key, dsa.DSAPublicKey): + return _rsa_security_strength(key.key_size) + + if isinstance(key, EllipticCurvePublicKey): + return _ecc_security_strength(key.curve.key_size) + + if isinstance( + key, + ( + ed25519.Ed25519PublicKey, + x25519.X25519PublicKey, + ), + ): + return 128 + + if isinstance( + key, + ( + ed448.Ed448PublicKey, + x448.X448PublicKey, + ), + ): + return 224 + + if isinstance(key, TradKEMPublicKey): + return estimate_key_security_strength(key._public_key) + + if isinstance(key, HybridPublicKey): + pq_strength = estimate_key_security_strength(getattr(key, "pq_key")) + trad_strength = estimate_key_security_strength(getattr(key, "trad_key")) + return min(pq_strength, trad_strength) + + else: + raise NotImplementedError(f"Security strength estimation not implemented for key type: {type(key)}") From deed3c0a54988f9f9293e9be54c9805e414fad51 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Sat, 17 Jan 2026 13:18:19 +0100 Subject: [PATCH 12/21] Reorder asymmetric imports in `security_utils.py` for consistency. --- resources/security_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/security_utils.py b/resources/security_utils.py index 717088e9..b280fa27 100644 --- a/resources/security_utils.py +++ b/resources/security_utils.py @@ -7,7 +7,7 @@ from typing import Optional, Union -from cryptography.hazmat.primitives.asymmetric import ed448, x448, ed25519, x25519, dsa, rsa +from cryptography.hazmat.primitives.asymmetric import dsa, ed448, ed25519, rsa, x448, x25519 from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey from pq_logic.keys.abstract_pq import PQKEMPublicKey, PQSignaturePublicKey From c30e07ab3f9549e60c6f026e9d0909be7ef80a14 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Sat, 17 Jan 2026 13:18:25 +0100 Subject: [PATCH 13/21] Expose `get_key_security_strength` keyword to estimate key security strength via `security_utils`. --- resources/keyutils.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/resources/keyutils.py b/resources/keyutils.py index 04e5f4df..e4f0376d 100644 --- a/resources/keyutils.py +++ b/resources/keyutils.py @@ -81,6 +81,7 @@ TRAD_SIG_NAME_2_OID, TRAD_STR_OID_TO_KEY_NAME, ) +from resources.security_utils import estimate_key_security_strength from resources.suiteenums import KeySaveType from resources.typingutils import PrivateKey, PublicKey, SignKey, TradPrivateKey, TradSignKey, TradVerifyKey, VerifyKey @@ -1453,3 +1454,33 @@ def get_pq_stateful_sig_index_from_sig( # noqa D417 undocumented-params return key.get_leaf_index(signature) raise NotImplementedError(f"Unsupported key type for signature index extraction. Got: {type(key).__name__}") + + +@keyword(name="Get Key Security Strength") +def get_key_security_strength( # noqa D417 undocumented-params + key: Union[PrivateKey, PublicKey], +) -> int: + """Return the estimated security strength (in bits) for the provided key. + + Arguments: + --------- + - `key`: The key instance to estimate the security strength for. + + Returns: + ------- + - The estimated security strength in bits. + + Raises: + ------ + - `ValueError`: If the security strength cannot be estimated for the given key type. + - `NotImplementedError`: If the key type is not supported for security strength estimation. + + Examples: + -------- + | ${strength}= | Get Key Security Strength | ${public_key} | + + """ + strength = estimate_key_security_strength(key) + if strength == 0: + raise ValueError(f"Could not estimate security strength for key type: {type(key)}") + return strength From bc8713292f0f135ccf9f311cbd126efdc7014f05 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Sat, 17 Jan 2026 13:19:36 +0100 Subject: [PATCH 14/21] Add unit tests for `get_key_security_strength` with various key types and sizes. --- .../__init__.py | 0 .../test_get_key_security_strength.py | 208 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 unit_tests/tests_security_strength_related/__init__.py create mode 100644 unit_tests/tests_security_strength_related/test_get_key_security_strength.py diff --git a/unit_tests/tests_security_strength_related/__init__.py b/unit_tests/tests_security_strength_related/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unit_tests/tests_security_strength_related/test_get_key_security_strength.py b/unit_tests/tests_security_strength_related/test_get_key_security_strength.py new file mode 100644 index 00000000..3f3d0a76 --- /dev/null +++ b/unit_tests/tests_security_strength_related/test_get_key_security_strength.py @@ -0,0 +1,208 @@ +import unittest + +from resources.keyutils import generate_key, get_key_security_strength + + +# TODO add test cases to get the security strength for HSS keys with different digest output sizes. + +class TestGetKeySecurityStrength(unittest.TestCase): + + def test_rsa_2048_key_strength(self): + """ + GIVEN an RSA 2048 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 112 bits. + """ + key = generate_key("rsa", length=2048) + self.assertEqual(get_key_security_strength(key), 112) + self.assertEqual(get_key_security_strength(key.public_key()), 112) + + def test_rsa_kem_2048_key_strength(self): + """ + GIVEN an RSA-KEM-2048 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 112 bits. + """ + key = generate_key("rsa-kem", length=2048) + self.assertEqual(get_key_security_strength(key), 112) + self.assertEqual(get_key_security_strength(key.public_key()), 112) + + def test_rsa_3072_key_strength(self): + """ + GIVEN an RSA 3072 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 128 bits. + """ + key = generate_key("rsa", length=3072) + self.assertEqual(get_key_security_strength(key), 128) + self.assertEqual(get_key_security_strength(key.public_key()), 128) + + def test_ecc_p256_key_strength(self): + """ + GIVEN an ECC P-256 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 128 bits. + """ + key = generate_key("ecc", curve="secp256r1") + self.assertEqual(get_key_security_strength(key), 128) + self.assertEqual(get_key_security_strength(key.public_key()), 128) + + def test_ecc_p384_key_strength(self): + """ + GIVEN an ECC P-384 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 192 bits. + """ + key = generate_key("ecc", curve="secp384r1") + self.assertEqual(get_key_security_strength(key), 192) + self.assertEqual(get_key_security_strength(key.public_key()), 192) + + def test_ed25519_key_strength(self): + """ + GIVEN an Ed25519 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 128 bits. + """ + key = generate_key("ed25519") + self.assertEqual(get_key_security_strength(key), 128) + self.assertEqual(get_key_security_strength(key.public_key()), 128) + + def test_x25519_key_strength(self): + """ + GIVEN an X25519 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 128 bits. + """ + key = generate_key("x25519") + self.assertEqual(get_key_security_strength(key), 128) + self.assertEqual(get_key_security_strength(key.public_key()), 128) + + def test_ed448_key_strength(self): + key = generate_key("ed448") + self.assertEqual(get_key_security_strength(key), 224) + self.assertEqual(get_key_security_strength(key.public_key()), 224) + + def test_x448_key_strength(self): + """ + GIVEN an X448 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 224 bits. + """ + key = generate_key("x448") + self.assertEqual(get_key_security_strength(key), 224) + self.assertEqual(get_key_security_strength(key.public_key()), 224) + + def test_xmss_key_sha256_strength(self): + """ + GIVEN an XMSS-SHA2-10-256 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 128 bits. + """ + key = generate_key("xmss-sha2_10_256") + # ensure that the pq_streng flag is always considered. + self.assertEqual(get_key_security_strength(key), 128) + self.assertEqual(get_key_security_strength(key.public_key()), 128) + + def test_xmss_key_sha192_strength(self): + """ + GIVEN an XMSS-SHA2-10-192 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 96 bits. + """ + key = generate_key("xmss-sha2_10_192") + self.assertEqual(get_key_security_strength(key), 96) + self.assertEqual(get_key_security_strength(key.public_key()), 96) + + def test_hss_key_shake_n24_m24_strength(self): + """ + GIVEN an HSS-SHAKE with LMS and LMOTS digest output size set to 24 bytes. + WHEN get_key_security_strength is called, + THEN is the returned security strength 96 bits. + """ + key = generate_key("hss_lms_shake_m24_h5_lmots_shake_n24_w4") + self.assertEqual(get_key_security_strength(key), 96) + self.assertEqual(get_key_security_strength(key.public_key()), 96) + + def test_hss_key_shake_n32_m32_strength(self): + """ + GIVEN an HSS-SHAKE with LMS and LMOTS digest output size set to 32 bytes. + WHEN get_key_security_strength is called, + THEN is the returned security strength 128 bits. + """ + key = generate_key("hss_lms_shake_m32_h5_lmots_shake_n32_w4") + self.assertEqual(get_key_security_strength(key), 128) + self.assertEqual(get_key_security_strength(key.public_key()), 128) + + def test_hss_key_sha2_n24_m24_strength(self): + """ + GIVEN an HSS-SHA2 with LMS and LMOTS digest output size set to 24 bytes. + WHEN get_key_security_strength is called, + THEN is the returned security strength 96 bits. + """ + key = generate_key("hss_lms_sha256_m24_h5_lmots_sha256_n24_w8") + self.assertEqual(get_key_security_strength(key), 96) + self.assertEqual(get_key_security_strength(key.public_key()), 96) + + def test_hss_key_sha2_n32_m32_strength(self): + """ + GIVEN an HSS-SHA2 with LMS and LMOTS digest output size set to 32 bytes. + WHEN get_key_security_strength is called, + THEN is the returned security strength 256 bits. + """ + key = generate_key("hss_lms_sha256_m32_h5_lmots_sha256_n32_w8") + self.assertEqual(get_key_security_strength(key), 128) + self.assertEqual(get_key_security_strength(key.public_key()), 128) + + def test_ml_kem_1024_key_strength(self): + """ + GIVEN an ML-KEM-1024 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 256 bits. + """ + key = generate_key("ml-kem-1024") + self.assertEqual(get_key_security_strength(key), 256) + self.assertEqual(get_key_security_strength(key.public_key()), 256) + + def test_ml_dsa_44_key_strength(self): + """ + GIVEN an ML-DSA-44 key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 256 bits. + """ + key = generate_key("ml-dsa-44") + self.assertEqual(get_key_security_strength(key), 192) + self.assertEqual(get_key_security_strength(key.public_key()), 192) + + def test_slh_dsa_sha2_128f_key_strength(self): + """ + GIVEN an SLH-DSA-SHA2-128f key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 128 bits. + """ + key = generate_key("slh-dsa-sha2-128f") + self.assertEqual(get_key_security_strength(key), 128) + self.assertEqual(get_key_security_strength(key.public_key()), 128) + + def test_slh_dsa_sha2_192f_key_strength(self): + """ + GIVEN an SLH-DSA-SHA2-192f key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 192 bits. + """ + key = generate_key("slh-dsa-sha2-192f") + self.assertEqual(get_key_security_strength(key), 192) + self.assertEqual(get_key_security_strength(key.public_key()), 192) + + def test_slh_dsa_sha2_256f_key_strength(self): + """ + GIVEN an SLH-DSA-SHA2-256f key. + WHEN get_key_security_strength is called, + THEN is the returned security strength 256 bits. + """ + key = generate_key("slh-dsa-sha2-256f") + self.assertEqual(get_key_security_strength(key), 256) + self.assertEqual(get_key_security_strength(key.public_key()), 256) + + +if __name__ == "__main__": + unittest.main() From c89a0fecc29b1a6e36b65aad28ae707398698dd3 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Sat, 17 Jan 2026 13:20:58 +0100 Subject: [PATCH 15/21] Add type annotations to `KM_KD_ALG` and `KDF_OID_2_NAME` for stricter type checking in `oidutils.py`. --- resources/oidutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/oidutils.py b/resources/oidutils.py index bb400fc2..bf4ae9bd 100644 --- a/resources/oidutils.py +++ b/resources/oidutils.py @@ -457,7 +457,7 @@ KM_KA_ALG.update(ECMQV) KM_KA_ALG_NAME_2_OID = {y: x for x, y in KM_KA_ALG.items()} -KM_KD_ALG = {rfc9481.id_PBKDF2: "pbkdf2"} # As per Section 4.4 in RFC 9481 +KM_KD_ALG: Dict[univ.ObjectIdentifier, str] = {rfc9481.id_PBKDF2: "pbkdf2"} # As per Section 4.4 in RFC 9481 KM_KW_ALG = { rfc9481.id_aes128_wrap: "aes128_wrap", rfc9481.id_aes192_wrap: "aes192_wrap", @@ -501,7 +501,7 @@ HKDF_OID_2_NAME = {v: k for k, v in HKDF_NAME_2_OID.items()} -KDF_OID_2_NAME = {} +KDF_OID_2_NAME: Dict[univ.ObjectIdentifier, str] = {} KDF_OID_2_NAME.update(KM_KD_ALG) KDF_OID_2_NAME.update(HKDF_OID_2_NAME) KDF_OID_2_NAME.update({rfc9690.id_kdf_kdf3: "kdf3", rfc9690.id_kdf_kdf2: "kdf2"}) From 70344b244b67d501d3f3d6b8f5952eba1780b73f Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Tue, 10 Feb 2026 13:20:20 +0100 Subject: [PATCH 16/21] Fix invalid doc for the PQ keys nist_level attribute. --- pq_logic/keys/abstract_pq.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pq_logic/keys/abstract_pq.py b/pq_logic/keys/abstract_pq.py index 01a53551..70d40b82 100644 --- a/pq_logic/keys/abstract_pq.py +++ b/pq_logic/keys/abstract_pq.py @@ -90,7 +90,7 @@ def sig_size(self) -> int: @property def nist_level(self) -> int: - """Return the claimed NIST security level as string.""" + """Return the claimed NIST security level as int.""" return int(self._sig_method.details["claimed_nist_level"]) @@ -169,7 +169,7 @@ def from_private_bytes(cls, data: bytes, name: str) -> "PQSignaturePrivateKey": @property def nist_level(self) -> int: - """Return the claimed NIST security level as string.""" + """Return the claimed NIST security level as int.""" return self.public_key().nist_level @@ -214,7 +214,7 @@ def encaps(self) -> Tuple[bytes, bytes]: @property def nist_level(self) -> int: - """Return the claimed NIST security level as string.""" + """Return the claimed NIST security level as int.""" return int(self._kem_method.details["claimed_nist_level"]) @@ -282,7 +282,7 @@ def public_key(self) -> PQKEMPublicKey: @property def nist_level(self) -> int: - """Return the claimed NIST security level as string.""" + """Return the claimed NIST security level as int.""" return self.public_key().nist_level @staticmethod From 6803a7f7b69ee49749f51a63c8b689f4209991a4 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Tue, 10 Feb 2026 13:20:43 +0100 Subject: [PATCH 17/21] Add nist_level method to ML-DSA and SLH-DSA classes for better doc description. --- pq_logic/keys/sig_keys.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pq_logic/keys/sig_keys.py b/pq_logic/keys/sig_keys.py index fcbcb4bb..54619c45 100644 --- a/pq_logic/keys/sig_keys.py +++ b/pq_logic/keys/sig_keys.py @@ -411,6 +411,10 @@ def seed_size(self) -> int: """Return the size of the seed used for ML-DSA key generation.""" return self._seed_size(name=self.name) + def nist_level(self) -> int: + """Return the NIST security level of the ML-DSA algorithm.""" + return self.public_key().nist_level + ########################## # SLH-DSA @@ -570,7 +574,7 @@ def from_public_bytes(cls, data: bytes, name: str) -> "SLHDSAPublicKey": @property def nist_level(self) -> int: - """Return the nist level.""" + """Return the NIST security level of the SLH-DSA algorithm.""" if oqs is not None: return super().nist_level if self.name.endswith("128s") or self.name.endswith("128f"): @@ -824,6 +828,9 @@ def seed_size(self) -> int: """Return the size of the seed used for SLH-DSA key generation.""" return self._seed_size(name=self.name) + def nist_level(self) -> int: + """Return the NIST security level of the SLH-DSA algorithm.""" + return self.public_key().nist_level ########################## # Falcon From 0f8acd6d407717d45722bdb0f3e4a54b4ce69412 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Tue, 10 Feb 2026 13:24:48 +0100 Subject: [PATCH 18/21] Run ruff format. --- pq_logic/keys/sig_keys.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pq_logic/keys/sig_keys.py b/pq_logic/keys/sig_keys.py index 54619c45..6e870ea1 100644 --- a/pq_logic/keys/sig_keys.py +++ b/pq_logic/keys/sig_keys.py @@ -832,6 +832,7 @@ def nist_level(self) -> int: """Return the NIST security level of the SLH-DSA algorithm.""" return self.public_key().nist_level + ########################## # Falcon ########################## From 299acca1622ec4eaf8b58a44324d009ea87e1a4d Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Tue, 10 Feb 2026 13:37:07 +0100 Subject: [PATCH 19/21] Update security strength documentation for RSA, DSA, ECC, and HSS key sizes. --- resources/security_utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/security_utils.py b/resources/security_utils.py index b280fa27..729a6f11 100644 --- a/resources/security_utils.py +++ b/resources/security_utils.py @@ -43,7 +43,7 @@ def _rsa_security_strength(key_size: int) -> int: - """Return an approximate security strength (in bits) for an RSA key size. + """Return an approximate security strength (in bits) for an RSA and DSA key size. Mapping follows NIST SP 800-57 Part 1 Rev. 5 Table 2. """ @@ -69,7 +69,7 @@ def _rsa_security_strength(key_size: int) -> int: def _ecc_security_strength(key_size: int) -> int: - """Return the security strength (in bits) for an ECC-style curve size. + """Return the security strength (in bits) for an ECC curve size. Mapping follows NIST SP 800-57 Part 1 Rev. 5 Table 2. """ @@ -100,6 +100,7 @@ def _get_pq_stfl_nist_security_strength(key: PQHashStatefulSigPublicKey) -> int: XMSS and XMSS^MT security strength is determined by the hash function output size. According to RFC 8391 Section 5. Parameter Sets. The security strength is halved when considering PQ security strength, because of the `Grover` algorithm. + The HSS key size is determined by the used parameter sets. :param key: The PQ stateful signature public key. :return: The security strength in bits. @@ -113,12 +114,12 @@ def _get_pq_stfl_nist_security_strength(key: PQHashStatefulSigPublicKey) -> int: return key.key_bit_security raise NotImplementedError( - f"Security strength estimation is only implemented for XMSS and XMSSMT keys. Got: {type(key)}" + f"Security strength estimation is only implemented for XMSS, XMSSMT and HSS keys. Got: {type(key)}" ) def _nist_level_strength(level: Optional[int]) -> int: - """Translate a claimed NIST level into an approximate security strength. + """Convert a claimed NIST level into an approximate security strength. Mapping follows NIST SP 800-57 Part 1 Rev. 5 Table 4. """ From 12a0fdda8af4a4e39edbe72ec18f889087911993 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Tue, 10 Feb 2026 13:46:59 +0100 Subject: [PATCH 20/21] Add not_keyword decorator to estimate_key_security_strength function --- resources/security_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/security_utils.py b/resources/security_utils.py index 729a6f11..6301eb67 100644 --- a/resources/security_utils.py +++ b/resources/security_utils.py @@ -9,6 +9,7 @@ from cryptography.hazmat.primitives.asymmetric import dsa, ed448, ed25519, rsa, x448, x25519 from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey +from robot.api.deco import not_keyword from pq_logic.keys.abstract_pq import PQKEMPublicKey, PQSignaturePublicKey from pq_logic.keys.abstract_stateful_hash_sig import PQHashStatefulSigPublicKey @@ -129,6 +130,7 @@ def _nist_level_strength(level: Optional[int]) -> int: return _NIST_LEVEL_TO_STRENGTH.get(int(level), 0) +@not_keyword def estimate_key_security_strength(key: Union[PrivateKey, PublicKey]) -> int: """Estimate the security strength of a key in bits. From 278026657750e3151de8f2e7f5d6f47829e84ae8 Mon Sep 17 00:00:00 2001 From: Guiliano99 Date: Tue, 10 Feb 2026 13:47:04 +0100 Subject: [PATCH 21/21] Add not_keyword decorator to compute_hss_signature_index function --- pq_logic/keys/stateful_sig_keys.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pq_logic/keys/stateful_sig_keys.py b/pq_logic/keys/stateful_sig_keys.py index bf251992..f2db9231 100644 --- a/pq_logic/keys/stateful_sig_keys.py +++ b/pq_logic/keys/stateful_sig_keys.py @@ -20,6 +20,7 @@ from pyhsslms.pyhsslms import LenI from pyhsslms.pyhsslms import lmots_params as PYHSSLMS_LMOTS_PARAMS from pyhsslms.pyhsslms import lms_params as PYHSSLMS_LMS_PARAMS +from robot.api.deco import not_keyword from pq_logic.keys.abstract_stateful_hash_sig import PQHashStatefulSigPrivateKey, PQHashStatefulSigPublicKey from resources.exceptions import InvalidKeyData @@ -284,6 +285,7 @@ def _compute_hss_signature_length(details: Dict[str, int], levels: int) -> int: return 4 + max(levels - 1, 0) * (per_sig + per_pub) + per_sig +@not_keyword def compute_hss_signature_index(signature: bytes, key: Union["HSSPrivateKey", "HSSPublicKey"]) -> int: """Return the global HSS signature index and the encoded depth.""" # TODO fix, if needed for different LMS and LMOTS key types.