Skip to content

OpenPGP: Normalization when hashing for Text-Mode Signatures #2553

Open

Description

@hko-s

When normalizing a data payload for an OpenPGP Text-Mode data signature (as described in https://www.rfc-editor.org/info/rfc9580/#name-text-signature-type-id-0x01), the RFC is not fully clear how exactly normalization should be performed.

However, the ecosystem-spanning "OpenPGP interoperability test suite" now has new test cases that effectively act as a survey of all implementations.
In this test, PGPainless (based on BC Java, cc @vanitasvitae) exhibits normalization of bare "CR" characters that doesn't interoperate with most other implementations.

See here for the relevant test results.

The issue is that BC normalizes bare "CR" bytes to "CR+LF".
Almost all other implementations, however, hash bare "CR" bytes unaltered.

I hope BC will consider adjusting its normalization to align with the common approach of leaving bare "CR" bytes unchanged during hashing.

Activity

  1. hko-s commented on Nov 4, 2026

    @hko-s
    Author

    FYI: There is a lot of additional context in this email thread in the IETF OpenPGP WG, if anyone is interested.
    Though I don't think it's necessary to read all of that, for this ticket 😅

  2. self-assigned this
    on Nov 4, 2026
  3. roy-basmacier commented on Nov 4, 2026

    @roy-basmacier

    Thanks @hko-s for the report and the interop-suite pointers. This should be in the beta soon.

    Text signatures (type 0x01): a CR not followed by LF is now hashed unaltered, so only a bare LF becomes CRLF. That matches GnuPG, GopenPGP, OpenPGP.js, rpgpie and the OpenPGP messages draft. PGPCanonicalizedDataGenerator also no longer turns a bare CR into CRLF in the text literal data it writes, so a signature over the original data still verifies over the literal data.

    Cleartext signatures: a line now ends only at LF or CRLF, and a bare CR is part of the text. A dash after a bare CR is no longer dash-escaped on output, and on input it is no longer un-escaped or taken as the start of the signature armor. The output now matches gpg 2.4.4 byte for byte.

    Compatibility:

    • When verifying, a text signature that fails over the new form is also checked against the old CR→CRLF form. That form is what earlier BC releases and Sequoia 2.x produce, so existing signatures keep verifying. Set org.bouncycastle.openpgp.text_sig_allow_legacy_cr=false to accept only the new form.
    • A cleartext message whose armor itself uses CR-only line endings, such as a classic Mac file, is still read with CR as its line ending. Setting org.bouncycastle.openpgp.csf_bare_cr_line_ending=true reads every cleartext message the old way.

    One remaining difference: GnuPG drops CRs right before a line ending or at the end of the data when hashing ("a\r" is hashed as "a"), so those particular GnuPG text signatures still won't verify here. No other implementation in the suite does that, so we haven't copied it.

  4. hko-s commented on Nov 4, 2026

    @hko-s
    Author

    Thank you for looking into this, and resolving the matter so swiftly!

    I did not realize that GnuPG drops CRs before line endings, and not just at the end of the file (if so, that sounds like parts of the cleartext business logic - which is supposed to skip hashing whitespace at the end of lines - might have seeped into GnuPG's "regular" text mode. Curious.)

    Your solution sounds sensible to me. As you suggest, not mirroring GnuPG's special handling of CR at the end of file (and lines?) seems like the best approach to me as well. Even though it's of course unfortunate that this means some edge cases will not be interoperable with GnuPG.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions