Skip to content

Commit d51b5ef

Browse files
authored
Merge pull request #23 from Guiliano99/UpdateSLHDSA
Update SLH-DSA to support liboqs.
2 parents 03d4174 + 6100812 commit d51b5ef

File tree

1 file changed

+139
-1
lines changed

1 file changed

+139
-1
lines changed

pq_logic/keys/sig_keys.py

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,35 @@
4747
FALCON_NAMES = ["falcon-512", "falcon-1024", "falcon-padded-512", "falcon-padded-1024"]
4848
ML_DSA_NAMES = ["ml-dsa-44", "ml-dsa-65", "ml-dsa-87"]
4949

50+
# Mapping of SLH-DSA algorithm names to their liboqs counterparts.
51+
SLH_DSA_LIBOQS_NAME_MAP = {
52+
"slh-dsa-sha2-128s": "SLH_DSA_PURE_SHA2_128S",
53+
"slh-dsa-sha2-128f": "SLH_DSA_PURE_SHA2_128F",
54+
"slh-dsa-sha2-192s": "SLH_DSA_PURE_SHA2_192S",
55+
"slh-dsa-sha2-192f": "SLH_DSA_PURE_SHA2_192F",
56+
"slh-dsa-sha2-256s": "SLH_DSA_PURE_SHA2_256S",
57+
"slh-dsa-sha2-256f": "SLH_DSA_PURE_SHA2_256F",
58+
"slh-dsa-shake-128s": "SLH_DSA_PURE_SHAKE_128S",
59+
"slh-dsa-shake-128f": "SLH_DSA_PURE_SHAKE_128F",
60+
"slh-dsa-shake-192s": "SLH_DSA_PURE_SHAKE_192S",
61+
"slh-dsa-shake-192f": "SLH_DSA_PURE_SHAKE_192F",
62+
"slh-dsa-shake-256s": "SLH_DSA_PURE_SHAKE_256S",
63+
"slh-dsa-shake-256f": "SLH_DSA_PURE_SHAKE_256F",
64+
# Pre-hash variants
65+
"slh-dsa-sha2-128s-sha256": "SLH_DSA_SHA2_256_PREHASH_SHA2_128S",
66+
"slh-dsa-sha2-128f-sha256": "SLH_DSA_SHA2_256_PREHASH_SHA2_128F",
67+
"slh-dsa-sha2-192s-sha512": "SLH_DSA_SHA2_512_PREHASH_SHA2_192S",
68+
"slh-dsa-sha2-192f-sha512": "SLH_DSA_SHA2_512_PREHASH_SHA2_192F",
69+
"slh-dsa-sha2-256s-sha512": "SLH_DSA_SHA2_512_PREHASH_SHA2_256S",
70+
"slh-dsa-sha2-256f-sha512": "SLH_DSA_SHA2_512_PREHASH_SHA2_256F",
71+
"slh-dsa-shake-128s-shake128": "SLH_DSA_SHAKE_128_PREHASH_SHAKE_128S",
72+
"slh-dsa-shake-128f-shake128": "SLH_DSA_SHAKE_128_PREHASH_SHAKE_128F",
73+
"slh-dsa-shake-192s-shake256": "SLH_DSA_SHAKE_256_PREHASH_SHAKE_192S",
74+
"slh-dsa-shake-192f-shake256": "SLH_DSA_SHAKE_256_PREHASH_SHAKE_192F",
75+
"slh-dsa-shake-256s-shake256": "SLH_DSA_SHAKE_256_PREHASH_SHAKE_256S",
76+
"slh-dsa-shake-256f-shake256": "SLH_DSA_SHAKE_256_PREHASH_SHAKE_256F",
77+
}
78+
5079

5180
class MLDSAPublicKey(PQSignaturePublicKey):
5281
"""Represent an ML-DSA public key."""
@@ -382,6 +411,32 @@ def seed_size(self) -> int:
382411
##########################
383412

384413

414+
def _get_liboqs_slh_dsa_name(alg_name: str, hash_alg: Optional[str]) -> str:
415+
"""Get the SLH-DSA algorithm name based on the base algorithm and hash algorithm.
416+
417+
:param alg_name: The base SLH-DSA algorithm name (e.g., "slh-dsa-sha2-128s").
418+
:param hash_alg: The optional hash algorithm name (e.g., "sha256").
419+
:return: The combined SLH-DSA algorithm name.
420+
"""
421+
alg_name = alg_name.lower()
422+
423+
if alg_name not in SLH_DSA_LIBOQS_NAME_MAP:
424+
raise ValueError(f"Invalid SLH-DSA algorithm name provided: {alg_name}.")
425+
426+
if hash_alg is None:
427+
return SLH_DSA_LIBOQS_NAME_MAP[alg_name]
428+
429+
hash_alg = hash_alg.lower()
430+
431+
if hash_alg in ["sha256", "sha512", "shake128", "shake256"]:
432+
combined_name = f"{alg_name}-{hash_alg}"
433+
if combined_name in SLH_DSA_PRE_HASH_NAME_2_OID:
434+
return SLH_DSA_LIBOQS_NAME_MAP[combined_name]
435+
raise ValueError(f"Invalid combination of SLH-DSA and hash algorithm: {combined_name}.")
436+
437+
raise ValueError(f"Invalid hash algorithm for SLH-DSA: {hash_alg}.")
438+
439+
385440
class SLHDSAPublicKey(PQSignaturePublicKey):
386441
"""Represent an SLH-DSA public key."""
387442

@@ -399,6 +454,14 @@ def _initialize_key(self) -> None:
399454
_other = self.name.replace("_", "-")
400455
self._slh_class: SLH_DSA = fips205.SLH_DSA_PARAMS[_other]
401456

457+
if oqs is not None:
458+
oqs_name = SLH_DSA_LIBOQS_NAME_MAP.get(self.name)
459+
if oqs_name is not None:
460+
try:
461+
self._sig_method = oqs.Signature(oqs_name)
462+
except Exception: # pragma: no cover - liboqs not available pylint: disable=broad-exception-caught
463+
self._sig_method = None
464+
402465
def _check_name(self, name: str) -> Tuple[str, str]:
403466
"""Check if the name is valid."""
404467
return name, name.replace("_", "-")
@@ -421,6 +484,26 @@ def check_hash_alg(self, hash_alg: Union[None, str, hashes.HashAlgorithm]) -> Op
421484
logging.info("%s does not support the hash algorithm: %s", self.name, hash_alg)
422485
return None
423486

487+
def _verify_oqs_signature(self, signature: bytes, data: bytes, ctx: bytes, hash_alg: Optional[str]) -> bool:
488+
"""Verify the signature using liboqs.
489+
490+
:param signature: The signature to verify.
491+
:param data: The data to verify.
492+
:param ctx: The context to add for the signature. Defaults to `b""`.
493+
:return: True if the signature is valid, False otherwise.
494+
:raises `InvalidSignature`: If the signature method is not initialized.
495+
"""
496+
if self._sig_method is None:
497+
raise ValueError("liboqs signature method is not initialized.")
498+
try:
499+
tmp_name = _get_liboqs_slh_dsa_name(alg_name=self.name, hash_alg=hash_alg)
500+
with oqs.Signature(tmp_name) as _sig_method:
501+
result = _sig_method.verify_with_ctx_str(data, signature, ctx, self._public_key_bytes)
502+
503+
except RuntimeError as exc:
504+
raise InvalidSignature() from exc
505+
return result
506+
424507
def verify(
425508
self,
426509
signature: bytes,
@@ -439,6 +522,19 @@ def verify(
439522
:raises InvalidSignature: If the signature is invalid.
440523
"""
441524
hash_alg = self.check_hash_alg(hash_alg=hash_alg)
525+
msg = "SLH-DSA Signature verification failed."
526+
527+
if len(ctx) > 255:
528+
raise ValueError(f"The context length is longer than 255 bytes. Got: {len(ctx)}")
529+
530+
if not is_prehashed and getattr(self, "_sig_method", None):
531+
logging.info("Verify SLH-DSA signature with `liboqs`.")
532+
533+
try:
534+
self._verify_oqs_signature(signature, data, ctx, hash_alg)
535+
except (RuntimeError, InvalidSignature) as exc:
536+
raise InvalidSignature(msg) from exc
537+
442538
if hash_alg is None:
443539
sig = self._slh_class.slh_verify(m=data, sig=signature, pk=self._public_key_bytes, ctx=ctx)
444540
else:
@@ -451,7 +547,7 @@ def verify(
451547
sig = self._slh_class.slh_verify_internal(m=mp, sig=signature, pk=self._public_key_bytes)
452548

453549
if not sig:
454-
raise InvalidSignature()
550+
raise InvalidSignature(msg)
455551

456552
@classmethod
457553
def from_public_bytes(cls, data: bytes, name: str) -> "SLHDSAPublicKey":
@@ -521,6 +617,20 @@ def _from_seed(alg_name: str, seed: Optional[bytes]) -> Tuple[bytes, bytes, byte
521617

522618
def _initialize_key(self) -> None:
523619
"""Initialize the SLH-DSA private key."""
620+
try:
621+
if oqs is not None:
622+
self._sig_method = oqs.Signature(SLH_DSA_LIBOQS_NAME_MAP[self.name], secret_key=self._private_key_bytes)
623+
if self._private_key_bytes is None and self._seed is None:
624+
logging.info("Generate SLH-DSA keypair with `liboqs`")
625+
print("Generate SLH-DSA keypair with `liboqs`")
626+
self._public_key_bytes = self._sig_method.generate_keypair()
627+
self._private_key_bytes = self._sig_method.export_secret_key()
628+
seed_size = self._seed_size(self.name)
629+
self._seed = self._private_key_bytes[:seed_size]
630+
631+
except Exception: # pragma: no cover - liboqs not available pylint: disable=broad-exception-caught
632+
self._sig_method = None
633+
524634
self._slh_class: SLH_DSA = fips205.SLH_DSA_PARAMS[self._other_name]
525635
if self._private_key_bytes is None and self._public_key_bytes is None:
526636
priv_key, pub_key, seed = self._from_seed(self.name, self._seed)
@@ -558,6 +668,26 @@ def public_key(self) -> SLHDSAPublicKey:
558668
"""
559669
return SLHDSAPublicKey(alg_name=self.name, public_key=self._public_key_bytes)
560670

671+
def _sign_with_oqs(self, data: bytes, ctx: bytes, hash_alg: Optional[str]) -> bytes:
672+
"""Sign the data using liboqs.
673+
674+
:param data: The data to sign.
675+
:param ctx: The context to add for the signature. Defaults to `b""`.
676+
:param hash_alg: The optional hash algorithm to use for the pre-hashing of the data.
677+
:return: The computed signature.
678+
:raises ValueError: If the signature method is not initialized.
679+
"""
680+
if self._sig_method is None:
681+
raise ValueError("liboqs signature method is not initialized.")
682+
try:
683+
tmp_name = _get_liboqs_slh_dsa_name(alg_name=self.name, hash_alg=hash_alg)
684+
logging.info("Sing data with: %s", tmp_name)
685+
with oqs.Signature(tmp_name, secret_key=self._private_key_bytes) as _sig_method:
686+
sig = _sig_method.sign_with_ctx_str(data, ctx)
687+
except RuntimeError as exc:
688+
raise ValueError("Could not sign the data with SLH-DSA") from exc
689+
return sig
690+
561691
def sign(
562692
self,
563693
data: bytes,
@@ -576,6 +706,14 @@ def sign(
576706
:raises ValueError: If the context is too long (255), or if the signature cannot be computed.
577707
"""
578708
hash_alg = self.check_hash_alg(hash_alg=hash_alg)
709+
710+
if len(ctx) > 255:
711+
raise ValueError(f"The context length is longer than 255 bytes. Got: {len(ctx)}")
712+
713+
if not is_prehashed and getattr(self, "_sig_method", None):
714+
logging.info("Sign SLH-DSA signature with `liboqs`.")
715+
return self._sign_with_oqs(data=data, ctx=ctx, hash_alg=hash_alg)
716+
579717
if hash_alg is None:
580718
sig = self._slh_class.slh_sign(m=data, sk=self._private_key_bytes, ctx=ctx)
581719

0 commit comments

Comments
 (0)