Skip to content

Ensure generated RSA key primes satisfy P < Q constraint#305

Merged
twiss merged 3 commits into
ProtonMail:mainfrom
jam-awake:jam/fix-rsa-p-q-order
May 12, 2026
Merged

Ensure generated RSA key primes satisfy P < Q constraint#305
twiss merged 3 commits into
ProtonMail:mainfrom
jam-awake:jam/fix-rsa-p-q-order

Conversation

@jam-awake
Copy link
Copy Markdown
Contributor

  • Adds a wrapper around rsa.GenerateKey that swaps the P and Q primes if P > Q and recomputes the precomputed values
  • Applies this change to generateRSAKeyWithPrimes as well but only if the key has 2 prime numbers because I don't know what's safe to do in this context.

Questions I have:

  1. Given the duplication of these two functions' implementations, should this be consolidated somewhere? Or is it fine to have the duplication?
  2. I'm not sure what tests should be written that verifies this work, so I've omitted those. Is that fine?

Fixes #184

Also, per your contributing guidelines:

  1. I certify that the contribution was created in whole by me

I asked an AI to investigate the problem and attempt to fix it. I then reviewed what it produced and edited it to something that I feel like I would have written. I'm not familiar with Go or cryptography, but this issue seems small enough that this method seemed appropriate.

@andgal-siren
Copy link
Copy Markdown

andgal-siren commented Mar 20, 2026

IMO since the established practice is to duplicate code between v1 and v2 it is reasonable to follow suit and duplicate the additional code, rather than attempting to deduplicate only these particular changes.

It would be nice to have a test for p<q, however since this will only fail at random it may not be appropriate to include it in the unit tests here...

@jam-awake
Copy link
Copy Markdown
Contributor Author

IMO since the established practice is to duplicate code between v1 and v2 it is reasonable to follow suit and duplicate the additional code, rather than attempting to deduplicate only these particular changes.

👍

It would be nice to have a test for p<q, however since this will only fail at random it may not be appropriate to include it in the unit tests here...

Yeah, that's why I didn't include them. That being said, https://github.com/jam-awake/gpg-verify-bug does have a few RSA keys exhibiting this very issue. Is it possible to force rsa.generateKey to create bad keys via generateRSAKeyWithPrimes?

@twiss
Copy link
Copy Markdown
Collaborator

twiss commented Mar 20, 2026

Thanks for the PR!

If you pass a deterministic config.Rand, you could test this deterministically (i.e. the same keys will be generated every run).

Also, could you please cite the new RFC 9580 rather than RFC 4880? 🙏

@jam-awake
Copy link
Copy Markdown
Contributor Author

Also, could you please cite the new RFC 9580 rather than RFC 4880?

Done. I should note there are other references to RFC 4880 in the repo, but I don't plan on fixing those in this PR.

If you pass a deterministic config.Rand, you could test this deterministically (i.e. the same keys will be generated every run).

I misread this. I saw config and thought that meant passing in a config value. And then I was confused as to how I would accomplish that since neither function takes such an argument. (Instead of thinking about that more, I focused on figuring out how to write a test.) But you're just saying I should provide a deterministic io.Reader. That would allow me to test both functions. Ok.

I also looked through the test file and saw ReadArmoredKeyRing. That will make it easier to use the primes from https://github.com/jam-awake/gpg-verify-bug.

Let me work on that, but I don't anticipate getting to this again until sometime next week.

Comment thread openpgp/v2/keys_test.go

for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
for i := 0; i < 20; i++ {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here's the context behind this loop.

generateRSAKey/generateRSAKeyWithPrimes calls rand.Prime to generate a prime number using Io.Reader. rand.Prime does so by running the following loop:

  1. Read data from the Reader. If there's not enough to fill the buffer, return io.ErrUnexpectedEOF.
  2. Generate a new number from the read data
  3. Call ProbablyPrime on that number. If it passes, return the number. Otherwise, jump to step 1.

Running this test without this loop will sometimes succeed and sometimes fail with Failed to generate RSA key: unexpected EOF. Presumably, a failing ProbablyPrime check will cause more data to be read from the reader than what it contains. And once we reach the end, the reader's EOF is converted to that error and rand.Prime returns.

At first, I thought one or both of p or q are not actually prime numbers. However, when I call ProbablyPrime(1_000) on p and q, neither number reports an issue. So, perhaps the issue is with the DeterministicReader implementation? I'm not sure.

Regardless, looping was my temporary solution to this problem. Per its docs, ProbablyPrime has a (1/4)^20 chance of returning a false positive (i.e. saying a number is a prime when it's non-prime). I thought attempting to generate a key 20 times might somehow cancel out that issue.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ProbablyPrime is itself randomized, so this makes some sense to me.

@jam-awake
Copy link
Copy Markdown
Contributor Author

@twiss I believe I've addressed all your feedback aside from my comment above.

@jam-awake
Copy link
Copy Markdown
Contributor Author

@twiss Any updates on this?

1 similar comment
@jam-awake
Copy link
Copy Markdown
Contributor Author

@twiss Any updates on this?

// RFC 9580 section 5.5.5.1 requires p < q for RSA keys.
// The Go standard library does not guarantee this, so we swap if needed
// and recompute the precomputed values.
if len(key.Primes) == 2 && key.Primes[0].Cmp(key.Primes[1]) > 0 {
Copy link
Copy Markdown
Member

@lubux lubux Apr 9, 2026

Choose a reason for hiding this comment

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

Cmp is not constant time and exposes a timing side-channel on the secret primes. But should be fine for key generation in this case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm reading this comment as, "This would be a problem, but not in this particular usage." Is that correct?

@jam-awake jam-awake force-pushed the jam/fix-rsa-p-q-order branch 2 times, most recently from 542fba8 to 4b8e1ad Compare April 9, 2026 16:13
- Adds a wrapper around `rsa.GenerateKey` that swaps the P and Q primes
  if P > Q and recomputes the precomputed values
- Applies this change to `generateRSAKeyWithPrimes` as well but only
  if the key has 2 prime numbers because I don't know what's safe
  to do in this context.
- Adds tests verifying prime swap occurs using 2048-bit key from
  https://github.com/jam-awake/gpg-verify-bug
@jam-awake jam-awake force-pushed the jam/fix-rsa-p-q-order branch from 4b8e1ad to bed1dc3 Compare April 9, 2026 16:19
@jam-awake
Copy link
Copy Markdown
Contributor Author

I apologize for force pushing, but since my previous commits were unsigned, this PR would not have been mergeable.

In fixing that problem, I also collapsed the commits into a single one. The PR is unchanged, but now it's mergeable.

@jam-awake
Copy link
Copy Markdown
Contributor Author

@lubux Any updates on this PR?

lubux
lubux previously approved these changes Apr 29, 2026
@jam-awake
Copy link
Copy Markdown
Contributor Author

@lubux The PR builds and is approved, but I can't merge it. I'm assuming one of the maintainers here needs to do so?

Copy link
Copy Markdown
Collaborator

@twiss twiss left a comment

Choose a reason for hiding this comment

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

Apologies for the long silence on my part, I was off last month. One more nitpick below.

Comment thread openpgp/v2/keys_test.go Outdated
Comment thread openpgp/v2/keys_test.go

for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
for i := 0; i < 20; i++ {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ProbablyPrime is itself randomized, so this makes some sense to me.

@jam-awake
Copy link
Copy Markdown
Contributor Author

@twiss I've addressed your feedback. Anything else?

Copy link
Copy Markdown
Collaborator

@twiss twiss left a comment

Choose a reason for hiding this comment

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

👍 Thanks!

@jam-awake
Copy link
Copy Markdown
Contributor Author

CI builds. I'm assuming one of the repo's maintainers here needs to merge the PR. Is that correct?

@twiss twiss merged commit 681b4e5 into ProtonMail:main May 12, 2026
9 checks passed
@jam-awake jam-awake deleted the jam/fix-rsa-p-q-order branch May 12, 2026 13:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated RSA keys may not be spec-compliant

4 participants