Skip to content

Commit 868e723

Browse files
dstebilaclaude
andcommitted
Add external-mu (extmu) support to Signature wrapper and tests
liboqs enables the ML-DSA external-mu variants (ML-DSA-{44,65,87}-extmu), which reuse the standard sign/verify C API but interpret the signed input as the externally computed mu rather than a raw message. - Add an is_extmu flag (instance attribute + details) derived from the algorithm name, and document the external-mu input semantics on sign()/verify(). The signing API stays uniform and algorithm-agnostic, matching liboqs (which exposes no dedicated external-mu function and no runtime mu length). - Parametrize the generic sign/verify tests to use a 64-byte message for extmu variants (mirroring liboqs's test_sig.c) and drop the "extmu" stopgap from disabled_sig_patterns (added in #152). - Add test_is_extmu_flag covering the new flag. Closes #151. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Douglas Stebila <dstebila@uwaterloo.ca>
1 parent 52adaf8 commit 868e723

2 files changed

Lines changed: 40 additions & 13 deletions

File tree

oqs/oqs.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,10 @@ def __init__(self, alg_name: str, secret_key: Union[int, bytes, None] = None) ->
657657
self.claimed_nist_level = self._sig.contents.claimed_nist_level
658658
self.euf_cma = self._sig.contents.euf_cma
659659
self.sig_with_ctx_support = bool(self._sig.contents.sig_with_ctx_support)
660+
# External-mu ("extmu") variants (e.g. ML-DSA-*-extmu) reuse the standard
661+
# sign/verify API but interpret the message input as the externally
662+
# computed mu rather than a raw message.
663+
self.is_extmu = "-extmu" in self.method_name.decode()
660664
self.length_public_key = self._sig.contents.length_public_key
661665
self.length_secret_key = self._sig.contents.length_secret_key
662666
self.length_signature = self._sig.contents.length_signature
@@ -669,6 +673,7 @@ def __init__(self, alg_name: str, secret_key: Union[int, bytes, None] = None) ->
669673
"is_suf_cma": bool(self.suf_cma),
670674
"supports_context_signing": bool(self.sig_with_ctx_support),
671675
"sig_with_ctx_support": bool(self.sig_with_ctx_support),
676+
"is_extmu": self.is_extmu,
672677
"length_public_key": int(self.length_public_key),
673678
"length_secret_key": int(self.length_secret_key),
674679
"length_signature": int(self.length_signature),
@@ -719,7 +724,12 @@ def sign(self, message: bytes) -> bytes:
719724
"""
720725
Signs the provided message and returns the signature.
721726
722-
:param message: the message to sign.
727+
For external-mu variants (is_extmu, e.g. ML-DSA-*-extmu), message is the
728+
externally computed mu rather than a raw message; liboqs enforces its
729+
length (64 bytes for ML-DSA).
730+
731+
:param message: the message to sign (the externally computed mu for
732+
external-mu variants).
723733
"""
724734
# Provide length to avoid extra null char
725735
c_message = ct.create_string_buffer(message, len(message))
@@ -746,7 +756,11 @@ def verify(self, message: bytes, signature: bytes, public_key: bytes) -> bool:
746756
"""
747757
Verify the provided signature on the message; returns True if valid.
748758
749-
:param message: the signed message.
759+
For external-mu variants (is_extmu, e.g. ML-DSA-*-extmu), message is the
760+
externally computed mu rather than a raw message.
761+
762+
:param message: the signed message (the externally computed mu for
763+
external-mu variants).
750764
:param signature: the signature on the message.
751765
:param public_key: the signer's public key.
752766
"""

tests/test_sig.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44
import oqs
55
from oqs.oqs import Signature, native
66

7-
# Sigs for which unit testing is disabled.
8-
#
9-
# The ML-DSA "external mu" (extmu) variants require an externally computed mu
10-
# and cannot be signed via the standard sign() API, so exclude them from the
11-
# generic sign/verify correctness tests. See
12-
# https://github.com/open-quantum-safe/liboqs-python/issues/151.
13-
disabled_sig_patterns = ["extmu"]
7+
# Sigs for which unit testing is disabled
8+
disabled_sig_patterns = []
149

1510
if platform.system() == "Windows":
1611
disabled_sig_patterns = [""]
1712

13+
# External-mu (extmu) variants interpret the signed input as the externally
14+
# computed mu rather than a raw message. For ML-DSA that mu is 64 bytes (FIPS
15+
# 204); liboqs's own test suite likewise hardcodes this length for extmu.
16+
EXTMU_MESSAGE_LEN = 64
17+
18+
19+
def _message_len(alg_name: str) -> int:
20+
return EXTMU_MESSAGE_LEN if "-extmu" in alg_name else 100
21+
1822

1923
def test_correctness() -> tuple[None, str]:
2024
for alg_name in oqs.get_enabled_sig_mechanisms():
@@ -34,7 +38,7 @@ def test_correctness_with_ctx_str() -> tuple[None, str]:
3438

3539
def check_correctness(alg_name: str) -> None:
3640
with oqs.Signature(alg_name) as sig:
37-
message = bytes(random.getrandbits(8) for _ in range(100))
41+
message = bytes(random.getrandbits(8) for _ in range(_message_len(alg_name)))
3842
public_key = sig.generate_keypair()
3943
signature = sig.sign(message)
4044
assert sig.verify(message, signature, public_key) # noqa: S101
@@ -49,6 +53,15 @@ def check_correctness_with_ctx_str(alg_name: str) -> None:
4953
assert sig.verify_with_ctx_str(message, signature, context, public_key) # noqa: S101
5054

5155

56+
def test_is_extmu_flag() -> None:
57+
"""The is_extmu flag is exposed consistently and set for external-mu variants."""
58+
for alg_name in oqs.get_enabled_sig_mechanisms():
59+
with oqs.Signature(alg_name) as sig:
60+
expected = "-extmu" in alg_name
61+
assert sig.is_extmu == expected # noqa: S101
62+
assert sig.details["is_extmu"] == expected # noqa: S101
63+
64+
5265
def test_sig_with_ctx_support_detection() -> None:
5366
"""
5467
Test that sig_with_ctx_support matches the C API and that sign_with_ctx_str
@@ -83,7 +96,7 @@ def test_wrong_message() -> tuple[None, str]:
8396

8497
def check_wrong_message(alg_name: str) -> None:
8598
with oqs.Signature(alg_name) as sig:
86-
message = bytes(random.getrandbits(8) for _ in range(100))
99+
message = bytes(random.getrandbits(8) for _ in range(_message_len(alg_name)))
87100
public_key = sig.generate_keypair()
88101
signature = sig.sign(message)
89102
wrong_message = bytes(random.getrandbits(8) for _ in range(len(message)))
@@ -99,7 +112,7 @@ def test_wrong_signature() -> tuple[None, str]:
99112

100113
def check_wrong_signature(alg_name: str) -> None:
101114
with oqs.Signature(alg_name) as sig:
102-
message = bytes(random.getrandbits(8) for _ in range(100))
115+
message = bytes(random.getrandbits(8) for _ in range(_message_len(alg_name)))
103116
public_key = sig.generate_keypair()
104117
signature = sig.sign(message)
105118
wrong_signature = bytes(random.getrandbits(8) for _ in range(len(signature)))
@@ -115,7 +128,7 @@ def test_wrong_public_key() -> tuple[None, str]:
115128

116129
def check_wrong_public_key(alg_name: str) -> None:
117130
with oqs.Signature(alg_name) as sig:
118-
message = bytes(random.getrandbits(8) for _ in range(100))
131+
message = bytes(random.getrandbits(8) for _ in range(_message_len(alg_name)))
119132
public_key = sig.generate_keypair()
120133
signature = sig.sign(message)
121134
wrong_public_key = bytes(random.getrandbits(8) for _ in range(len(public_key)))

0 commit comments

Comments
 (0)