Hi Arpan,

Thanks for pointing out the difference. Our (built-in OpenJDK providers) 
principle is:

- if the signature itself has an invalid structure, we throw an exception; 
otherwise,
- if the signature cannot be verified by the public key, we return false.

This has been true since DSA. I think it is also consistent with the spec [1]:

* @throws SignatureException if this {@code Signature} object is not
* initialized properly, ****the passed-in signature is improperly
* encoded or of the wrong type****, if this signature algorithm is unable to
* process the input data provided, etc.

That said, I wouldn't say BC or ACCP is incorrect. It's even not a bad thing 
that we behave differently. Users should be prepared to deal with both outcomes 
in their applications. After all, SignatureException is declared there and you 
have to handle a checked exception anyway.

Thanks,
Weijun

[1] 
https://github.com/openjdk/jdk/blob/0381286adedbd7daf1eb01a820ad8179dbb7da15/src/java.base/share/classes/java/security/Signature.java#L826

> On Jul 24, 2026, at 11:47, Arpan Sharma <[email protected]> wrote:
> 
> Hi,
> 
> Following up with a new data point.
> 
> Since posting I added a third independent ML-DSA verifier to the same
> byte-identical differential: the Amazon Corretto Crypto Provider
> (ACCP 2.5.0, built on AWS-LC), alongside BouncyCastle 1.85 and the
> SUN provider, on JDK 26.0.1. ACCP agrees with BouncyCastle. It returns
> false on every structurally malformed signature and throws nothing.
> So across three implementations from independent lineages (SUN, BC's
> pure Java code, AWS-LC's C), SUN is the outlier on all 21 divergent
> rows (7 structural mutations x 3 parameter sets). The same-length,
> decodable-but-wrong cases still agree (false on all three).
> 
> mutation | BC | SUN | ACCP
> truncate-1 / truncate-half | false | SignatureException | false
> extend-1 / extend-block | false | SignatureException | false
> flip-last-bit / ones-all | false | SignatureException | false
> empty | false | SignatureException | false
> flip-first-bit / zero-all | false | false | false
> (same length, decodable, wrong)
> 
> To rule out the obvious artifact, each verify is pinned to its
> provider: the run prints the provider actually servicing the Signature
> and KeyFactory plus the concrete key class, and the three are distinct
> native types (BCMLDSAPublicKey, NamedX509Key, EvpMlDsaPublicKey), so
> there is no silent fallback or cross-provider key reuse. Harness,
> provenance check, verbatim output, and the pinned environment (exact
> Maven Central artifacts with sha256s) are at:
> 
> https://github.com/Arpan0995/pqc-provider-differential/tree/main/threeway-accp
> 
> I raise it here because it bears on the question I asked. SUN's
> throw-on-malformed path does not look like the common contract for
> ML-DSA verify(), given that two of the three providers return false,
> so a short note in the SUN provider documentation about which inputs
> surface as SignatureException versus a false return would help anyone
> writing portable code against verify(). I'm not arguing SUN is wrong
> (the javadoc permits both), just that the divergence is worth pinning
> down one way or the other. Still glad to share the harness or
> contribute test vectors that lock in whatever behaviour is decided.
> 
> Best regards,
> Arpan Sharma
> 

Reply via email to