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
>