Skip to content

ffi: add rnp_key_protect_ex() with Argon2 + AEAD support - #2432

Merged
ronaldtse merged 1 commit into
pqc-v6-argon2-testsfrom
pqc-standalone-and-argon2-ffi
Aug 20, 2026
Merged

ffi: add rnp_key_protect_ex() with Argon2 + AEAD support#2432
ronaldtse merged 1 commit into
pqc-v6-argon2-testsfrom
pqc-standalone-and-argon2-ffi

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

Summary

Addresses the second of two gaps flagged in PR #2431's "out of scope" section: the FFI did not expose Argon2 S2K for rnp_key_protect. This PR adds rnp_key_protect_ex() and a new public rnp_protection_params_t struct that lets callers opt into Argon2 + AEAD protection.

What's in this PR

  • include/rnp/rnp.h: new public struct rnp_protection_params_t and new FFI function rnp_key_protect_ex(handle, password, params). Wrapped in #ifndef RNP_PROTECTION_PARAMS_T_DEFINED because rnp.h lacks a top-level include guard (the existing internal rnp_key_protection_params_t lives in src/lib/types.h).
  • src/lib/rnp.cpp: full implementation. Strings are resolved to the internal enums (PGP_S2KS_ARGON2, PGP_AEAD_OCB etc.); defaults mirror rnp_key_protect(). Argon2 memory is given in KiB and converted to the on-wire log2(bytes) representation. On builds without ENABLE_CRYPTO_REFRESH, requests for Argon2 or explicit AEAD return RNP_ERROR_NOT_SUPPORTED.
  • src/tests/ffi-enc.cpp: new test test_ffi_key_protect_ex_argon2_roundtrip — generates a fresh RSA key, protects with explicit small Argon2 parameters, verifies wrong-password fails and correct-password succeeds, signs and verifies.

Why

The Argon2 + AEAD path was previously reachable only via the JSON keygen parser. Callers building keys via the regular FFI (rnp_op_generate + rnp_key_protect) had no way to request Argon2 S2K — they were stuck with iterated-and-salted + CFB even on v6 keys where the library forces Argon2 anyway, but only for v6.

This is also a prerequisite for the Thunderbird key-backup proposal (Kai Engert's key-backup-recovery-strategy.md) — it stores the recovery private key under "normal Thunderbird key protection", and being able to specify Argon2 via FFI matches the v6 expectation.

Standalone PQC variants — DEFERRED

The first "out of scope" gap from #2431 was standalone ML-DSA / ML-KEM / additional SLH-DSA variants (192f, 192s, 256f). This PR does NOT address them. Reasons:

  • Allocating new PGP_PKA_* codepoints requires verifying against the current draft-ietf-openpgp-pqc (the merged Update PQC Draft to Version 12 #2355 covered the draft-12 codepoints 30–36; the missing variants need codepoints that the draft may or may not have registered with IANA yet).
  • The wire format for standalone ML-KEM / ML-DSA differs from the composite variants already in rnp; needs careful spec reading.
  • The Botan backend already supports all 6 SLH-DSA parameter sets (SLHDSA192Small, SLHDSA192Fast, SLHDSA256Fast); only the OpenPGP wire layer is missing.
  • Local build is blocked by a pre-existing Botan 3.12 incompatibility in src/lib/crypto/ec.cpp:202 (uses Botan::EC_Group members that became opaque in 3.12), so I cannot verify PQC changes locally without first fixing that.

I'll open a follow-up issue/PR for standalone PQC once the codepoint question is resolved with the spec author.

Test plan

  • Build clean (no warnings) on default config
  • CI green on Botan + crypto-refresh legs
  • test_ffi_key_protect_ex_argon2_roundtrip passes (CI will run it; local build blocked by the Botan 3.12 issue mentioned above)

Out of scope for follow-up PRs

  • Standalone ML-DSA / ML-KEM / SLH-DSA 192f/192s/256f
  • New rnp.h top-level include guard (would touch the public header globally)
  • Replacing rnp_key_protect's parameter list with the new struct (would be API-breaking; current approach is additive)

Comment thread src/lib/rnp.cpp
unsigned iterations = 0;
pgp_s2k_specifier_t s2k_specifier = PGP_S2KS_ITERATED_AND_SALTED;
pgp_s2k_usage_t s2k_usage = PGP_S2KU_ENCRYPTED_AND_HASHED;
pgp_aead_alg_t aead_alg = PGP_AEAD_NONE;
Comment thread src/lib/rnp.cpp
pgp_s2k_specifier_t s2k_specifier = PGP_S2KS_ITERATED_AND_SALTED;
pgp_s2k_usage_t s2k_usage = PGP_S2KU_ENCRYPTED_AND_HASHED;
pgp_aead_alg_t aead_alg = PGP_AEAD_NONE;
uint8_t argon2_t = 0;
Comment thread src/lib/rnp.cpp
pgp_s2k_usage_t s2k_usage = PGP_S2KU_ENCRYPTED_AND_HASHED;
pgp_aead_alg_t aead_alg = PGP_AEAD_NONE;
uint8_t argon2_t = 0;
uint8_t argon2_p = 0;
Comment thread src/lib/rnp.cpp
pgp_aead_alg_t aead_alg = PGP_AEAD_NONE;
uint8_t argon2_t = 0;
uint8_t argon2_p = 0;
uint8_t argon2_m = 0;
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 74 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.09%. Comparing base (953d2e0) to head (56a427c).

Files with missing lines Patch % Lines
src/lib/rnp.cpp 0.00% 74 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2432      +/-   ##
==========================================
- Coverage   85.36%   85.09%   -0.28%     
==========================================
  Files         126      126              
  Lines       22861    22942      +81     
==========================================
+ Hits        19516    19523       +7     
- Misses       3345     3419      +74     

☔ 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
ronaldtse requested review from antonsviridenko and ni4 July 31, 2026 16:20
@ronaldtse
ronaldtse force-pushed the pqc-standalone-and-argon2-ffi branch from 90e6f55 to 56a427c Compare August 10, 2026 02:09
@ronaldtse
ronaldtse force-pushed the pqc-standalone-and-argon2-ffi branch from 56a427c to ef392af Compare August 20, 2026 10:11
Adds a new public FFI function rnp_key_protect_ex() that lets callers
opt into Argon2 S2K and AEAD encryption of secret-key material. The
existing rnp_key_protect() function exposes only the legacy
iterated-and-salted + CFB path; the Argon2 + AEAD path was reachable
only via JSON keygen, which is awkward for non-JSON callers.

New public types:
- rnp_protection_params_t with fields for cipher, cipher_mode, hash,
  iterations, s2k_type, aead_alg, argon2_t/p/m_kib.

Behaviour:
- s2k_type = "Argon2" forces AEAD usage (per RFC 9580 crypto refresh)
  and requires ENABLE_CRYPTO_REFRESH at build time. Without it, the
  function returns RNP_ERROR_NOT_SUPPORTED.
- aead_alg can be specified independently for non-Argon2 AEAD.
- All string fields accept NULL for sensible defaults (mirrors
  rnp_key_protect()).
- The Argon2 memory parameter is given in KiB and converted to the
  on-wire log2(bytes) representation.

Test: test_ffi_key_protect_ex_argon2_roundtrip generates a fresh RSA
key, protects it with explicit Argon2 parameters (small m/t/p for CI
speed), unlocks with wrong (fails) and correct (succeeds) passwords,
and confirms the key is still usable for signing after the round-trip.

Standalone PQC keygen (ML-DSA, ML-KEM standalone; SLH-DSA 192f/192s/
256f) is intentionally NOT in this PR. Adding it requires:
- Allocating new PGP_PKA_* codepoints per draft-ietf-openpgp-pqc
- Verifying wire-format details against the latest draft
- Material parsing/serialisation in key_material.cpp
- New fixtures from the draft test vector appendix
The Botan backend already supports these parameter sets
(SLHDSA192Small/Fast, SLHDSA256Fast); only the OpenPGP wire layer is
missing. Tracked as a follow-up.
@ronaldtse
ronaldtse force-pushed the pqc-standalone-and-argon2-ffi branch from ef392af to 6fd3b62 Compare August 20, 2026 10:37
@ronaldtse
ronaldtse changed the base branch from main to pqc-v6-argon2-tests August 20, 2026 10:37
@ronaldtse
ronaldtse merged commit 6fd3b62 into pqc-v6-argon2-tests Aug 20, 2026
9 of 101 checks passed
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.

2 participants