Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scripts/build_ffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,9 @@ def build_ffi(local_wolfssl, features):
int wc_dilithium_export_public(dilithium_key* key, byte* out, word32* outLen);
int wc_dilithium_import_public(const byte* in, word32 inLen, dilithium_key* key);
int wc_dilithium_sign_msg(const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, WC_RNG* rng);
int wc_dilithium_sign_ctx_msg(const byte* ctx, byte ctxLen, const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, WC_RNG* rng);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new FFI declarations use different types for ctxLen: wc_dilithium_sign_ctx_msg declares ctxLen as byte, but wc_dilithium_verify_ctx_msg declares ctxLen as word32. Please confirm the correct signature in the wolfSSL headers and make these consistent; if ctxLen is actually word32, the current byte declaration will truncate lengths >255 at the ABI boundary.

Suggested change
int wc_dilithium_sign_ctx_msg(const byte* ctx, byte ctxLen, const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, WC_RNG* rng);
int wc_dilithium_sign_ctx_msg(const byte* ctx, word32 ctxLen, const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, WC_RNG* rng);

Copilot uses AI. Check for mistakes.
int wc_dilithium_verify_msg(const byte* sig, word32 sigLen, const byte* msg, word32 msgLen, int* res, dilithium_key* key);
int wc_dilithium_verify_ctx_msg(const byte* sig, word32 sigLen, const byte* ctx, word32 ctxLen, const byte* msg, word32 msgLen, int* res, dilithium_key* key);
typedef dilithium_key MlDsaKey;
int wc_MlDsaKey_GetPrivLen(MlDsaKey* key, int* len);
int wc_MlDsaKey_GetPubLen(MlDsaKey* key, int* len);
Expand Down
18 changes: 18 additions & 0 deletions tests/test_mldsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,21 @@ def test_sign_verify(mldsa_type, rng):
# Verify with wrong message
wrong_message = b"This is a wrong message for ML-DSA signature"
assert not mldsa_pub.verify(signature, wrong_message)

# Verify with ctx for signature generated without
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor test comment clarity: "Verify with ctx for signature generated without" reads incomplete; consider updating to something like "...generated without context" so the intent is unambiguous.

Suggested change
# Verify with ctx for signature generated without
# Verify with ctx for signature generated without context

Copilot uses AI. Check for mistakes.
ctx = b"This is a test context for ML-DSA signature"
wrong_ctx = b"This is a wrong context for ML-DSA signature"
assert not mldsa_pub.verify(signature, message, ctx=wrong_ctx)

# Sign a message with context
signature = mldsa_priv.sign(message, rng, ctx=ctx)
assert len(signature) == mldsa_priv.sig_size

# Verify the signature by MlDsaPrivate
assert mldsa_priv.verify(signature, message, ctx=ctx)

# Verify the signature by MlDsaPublic
assert mldsa_pub.verify(signature, message, ctx=ctx)

# Verify with wrong ctx
assert not mldsa_pub.verify(signature, message, ctx=wrong_ctx)
66 changes: 48 additions & 18 deletions wolfcrypt/ciphers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2124,27 +2124,42 @@ def _encode_pub_key(self):

return _ffi.buffer(pub_key, out_size[0])[:]

def verify(self, signature, message):
def verify(self, signature, message, ctx=None):
"""
:param signature: signature to be verified
:type signature: bytes or str
:param message: message to be verified
:type message: bytes or str
:param ctx: context (optional)
:type ctx: None for no context, str or bytes otherwise
:return: True if the verification is successful, False otherwise
:rtype: bool
"""
sig_bytestype = t2b(signature)
msg_bytestype = t2b(message)
res = _ffi.new("int *")

ret = _lib.wc_dilithium_verify_msg(
_ffi.from_buffer(sig_bytestype),
len(sig_bytestype),
_ffi.from_buffer(msg_bytestype),
len(msg_bytestype),
res,
self.native_object,
)
if ctx is not None:
ctx_bytestype = t2b(ctx)
ret = _lib.wc_dilithium_verify_ctx_msg(
_ffi.from_buffer(sig_bytestype),
len(sig_bytestype),
_ffi.from_buffer(ctx_bytestype),
len(ctx_bytestype),
_ffi.from_buffer(msg_bytestype),
len(msg_bytestype),
res,
self.native_object,
)
else:
ret = _lib.wc_dilithium_verify_msg(
_ffi.from_buffer(sig_bytestype),
len(sig_bytestype),
_ffi.from_buffer(msg_bytestype),
len(msg_bytestype),
res,
self.native_object,
)

if ret < 0: # pragma: no cover
raise WolfCryptError("wc_dilithium_verify_msg() error (%d)" % ret)
Expand Down Expand Up @@ -2246,12 +2261,14 @@ def decode_key(self, priv_key, pub_key=None):
if pub_key is not None:
self._decode_pub_key(pub_key)

def sign(self, message, rng=Random()):
def sign(self, message, rng=Random(), ctx=None):
"""
:param message: message to be signed
:type message: bytes or str
:param rng: random number generator for sign
:type rng: Random
:param ctx: context (optional)
:type ctx: None for no context, str or bytes otherwise
:return: signature
:rtype: bytes
"""
Expand All @@ -2261,14 +2278,27 @@ def sign(self, message, rng=Random()):
out_size = _ffi.new("word32 *")
out_size[0] = in_size

ret = _lib.wc_dilithium_sign_msg(
_ffi.from_buffer(msg_bytestype),
len(msg_bytestype),
signature,
out_size,
self.native_object,
rng.native_object,
)
if ctx is not None:
ctx_bytestype = t2b(ctx)
ret = _lib.wc_dilithium_sign_ctx_msg(
_ffi.from_buffer(ctx_bytestype),
len(ctx_bytestype),
_ffi.from_buffer(msg_bytestype),
Comment on lines +2281 to +2286
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wc_dilithium_sign_ctx_msg() takes ctxLen as a C "byte" in the FFI definition, but this wrapper passes len(ctx_bytestype) without bounds checking. For ctx lengths > 255, cffi will truncate the value and only a prefix of ctx will be used, which is very surprising and can break domain separation; please validate ctx length (or adjust the FFI signature if ctxLen is actually wider).

Copilot uses AI. Check for mistakes.
len(msg_bytestype),
signature,
out_size,
self.native_object,
rng.native_object,
)
else:
ret = _lib.wc_dilithium_sign_msg(
_ffi.from_buffer(msg_bytestype),
len(msg_bytestype),
signature,
out_size,
self.native_object,
rng.native_object,
)

if ret < 0: # pragma: no cover
raise WolfCryptError("wc_dilithium_sign_msg() error (%d)" % ret)
Comment on lines 2303 to 2304
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When ctx is provided, failures will raise an exception that still references wc_dilithium_sign_msg(), even though wc_dilithium_sign_ctx_msg() was called. Please update the error string to match the actual call path (or include ctx/no-ctx in the message).

Copilot uses AI. Check for mistakes.
Expand Down
Loading