Skip to content

Export both traditional and PQC subkeys in rnp_key_export_autocrypt - #2482

Open
kaie wants to merge 2 commits into
rnpgp:mainfrom
kaie:autocrypt/pqc-export-subkey
Open

Export both traditional and PQC subkeys in rnp_key_export_autocrypt#2482
kaie wants to merge 2 commits into
rnpgp:mainfrom
kaie:autocrypt/pqc-export-subkey

Conversation

@kaie

@kaie kaie commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

When exporting a key for Autocrypt without specifying a subkey, auto-select up to two encryption subkeys: one traditional (non-PQC) and one PQC (ML-KEM-768+X25519 for v4 keys). If only a PQC subkey exists, export just that one. The resulting Autocrypt payload is a 5-packet sequence for the single-subkey case or a 7-packet sequence when both types are present.

Adds a test vector (EdDSA + ECDH Curve25519 + ML-KEM-768+X25519) and a corresponding test guarded by ENABLE_PQC.

The change implements the suggestion discussed here:

https://lists.hostpoint.ch/archives/list/openpgp-email@enigmail.net/thread/SB56PWOWRURNX5MZI2UBWEC3OVS5WMEC/

autocrypt/autocrypt#461

When exporting a key for Autocrypt without specifying a subkey, auto-select
up to two encryption subkeys: one traditional (non-PQC) and one PQC
(ML-KEM-768+X25519 for v4 keys). If only a PQC subkey exists, export just
that one. The resulting Autocrypt payload is a 5-packet sequence for the
single-subkey case or a 7-packet sequence when both types are present.

Adds a test vector (EdDSA + ECDH Curve25519 + ML-KEM-768+X25519) and a
corresponding test guarded by ENABLE_PQC.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.62500% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.43%. Comparing base (100cc8c) to head (a917f2e).

Files with missing lines Patch % Lines
src/lib/key.cpp 30.76% 9 Missing ⚠️
src/lib/rnp.cpp 89.47% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2482      +/-   ##
==========================================
- Coverage   85.46%   85.43%   -0.04%     
==========================================
  Files         125      125              
  Lines       22962    22988      +26     
==========================================
+ Hits        19625    19640      +15     
- Misses       3337     3348      +11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ronaldtse

Copy link
Copy Markdown
Contributor

Thanks Kai! CI is green apart from the two known fleet-wide fuzz jobs, but clang-format flags two trivial rewraps. Easiest fix:

clang-format -i src/lib/rnp.cpp src/tests/ffi-key.cpp

The two hunks it wants:

--- a/src/lib/rnp.cpp
+++ b/src/lib/rnp.cpp
-            rnp::Key *cur =
-              key->ffi->key_provider.request_key(search, PGP_OP_ENCRYPT, false);
+            rnp::Key *cur = key->ffi->key_provider.request_key(search, PGP_OP_ENCRYPT, false);

--- a/src/tests/ffi-key.cpp
+++ b/src/tests/ffi-key.cpp
-    assert_rnp_success(rnp_input_from_path(
-      &input, "data/test_key_edge_cases/eddsa-ecdh-mlkem-pub.pgp"));
+    assert_rnp_success(
+      rnp_input_from_path(&input, "data/test_key_edge_cases/eddsa-ecdh-mlkem-pub.pgp"));

Everything else looks in good shape - we reviewed the auto-selection logic (the is_pqc split, latest-binding checks, and the PQC-only fallback to a 5-packet export) and it reads clean against the autocrypt#461 discussion. Happy to take the second review once the format fix is in.

@ronaldtse ronaldtse left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed and approved - thank you Kai!

What was checked:

  • the auto-selection logic in rnp_key_export_autocrypt: the is_pqc() split with newest-creation tie-breaks for both the traditional and PQC picks, and the PQC-only fallback to a plain 5-packet export read correct;
  • write_autocrypt's all-or-nothing write with the new 7-packet sequence, including the public-key and valid-binding guards for the extra subkey;
  • the explicit-subkey path keeps its original single-subkey semantics;
  • the test vector (EdDSA + ECDH Curve25519 + ML-KEM-768+X25519) and the ENABLE_PQC-gated test cover both the dual-subkey and PQC-only cases.

Only remaining item is the trivial clang-format commit discussed above - everything else is green apart from the two fleet-wide fuzz jobs.

@ni4 could you take the second review/approval? This is a Thunderbird-driven follow-up to the autocrypt PQC export discussion (autocrypt/autocrypt#461), extending the machinery from Kai's own #2386.

@ronaldtse
ronaldtse requested a review from ni4 September 3, 2026 15:14

@ni4 ni4 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall LGTM, just two minor comments regarding missing #ifdef ENABLE_PQC for a pqc subkey.

Comment thread src/lib/key.cpp
RNP_LOG("Public key required");
return false;
}
Signature *pqc_binding = nullptr;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@kaie Could you please wrap this in #ifdef ENABLE_PQC? Without it we'll have coverage degradation for non-PQC builds.

Comment thread src/lib/key.cpp
cert->sig.write(memdst.dst());
sub.pkt().write(memdst.dst());
binding->sig.write(memdst.dst());
if (pqc_sub) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as the previous.

@ronaldtse

Copy link
Copy Markdown
Contributor

Agreed with @ni4 here - since the auto-selection in rnp_key_export_autocrypt already compiles out the is_pqc() branch for non-PQC builds, these two blocks would be permanently-uncovered lines there. Kai, here is the ready-made patch if it saves you a round-trip:

--- a/src/lib/key.cpp
+++ b/src/lib/key.cpp
@@ -1477,6 +1477,7 @@
         RNP_LOG("Public key required");
         return false;
     }
+#if defined(ENABLE_PQC)
     Signature *pqc_binding = nullptr;
     if (pqc_sub) {
         if (pqc_sub->is_secret()) {
@@ -1487,6 +1488,7 @@
             return false;
         }
     }
+#endif
 
     try {
         /* write all or nothing */
@@ -1498,10 +1500,12 @@
         cert->sig.write(memdst.dst());
         sub.pkt().write(memdst.dst());
         binding->sig.write(memdst.dst());
+#if defined(ENABLE_PQC)
         if (pqc_sub) {
             pqc_sub->pkt().write(memdst.dst());
             pqc_binding->sig.write(memdst.dst());
         }
+#endif
         dst_write(&dst, memdst.memory(), memdst.writeb());
         return !dst.werr;

Also handles ni4's "same as the previous" thread below. The pqc_sub parameter itself can stay unconditionally - it is harmless as an always-null default argument.

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.

3 participants