Skip to content

FFI parity: 89 new bindings — verification, key certification, security profiles#94

Open
ronaldtse wants to merge 14 commits into
mainfrom
ffi-parity
Open

FFI parity: 89 new bindings — verification, key certification, security profiles#94
ronaldtse wants to merge 14 commits into
mainfrom
ffi-parity

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

Summary

FFI-parity pass against librnp 0.18.1/main. The gem attaches FFI functions through an optional-probing hash (with HAVE_* constants); an audit against the 296-symbol FFI surface found 112 genuinely unbound exported functions. This PR binds 89 of them with full specs. (Remaining 23: 4 deliberate skips — stdio helpers and Curve25519 tweak internals — and 19 deprioritized items listed below. The crypto-refresh/PQC symbols are not exported by default builds and are not bound.)

What was added

  • Verification/signature introspection: Verify#flags=/#format; recipient enumeration (Rnp::Verify::Recipient with keyid/alg, used_recipient) and symenc twins (cipher/aead/hash/s2k); Rnp::Signature gains signature_type, fingerprint, expiration_time, features, key_flags, key_expiration, primary_uid?, key_server(+prefs), revoker, revocation_reason, trust_level, preferred_ciphers/hashes/compressions, valid?(revalidate:), export(armored:); subpacket API as eager value objects (Rnp::Signature::Subpacket: type/data/hashed?/critical?) with each_subpacket/subpackets/subpacket(); Key#remove_signature.
  • Key certification API (0.18): Rnp::KeySignature builder (all rnp_key_signature_set_* + add_preferred_* + #sign), created via Key#start_certification(uid, type:), #start_direct_signature, #start_revocation_signature; Key#remove_signatures(invalid:, unknown_key:, non_self:); Key#revocation_signature.
  • Security profile API (lib/rnp/security.rb): Rnp#add_security_rule(type:, name:, level:, from:, override:, usage:), #security_rule, #remove_security_rule, #timestamp= — feature-gated via HAVE_*. Spec demonstrates the real semantics: SHA-1 verification rejected by default → allowed via override rule → rejected again after removal (librnp's rules gate hash/cipher verification, not key sizes).
  • Key inspection batch: Key#version, #expired?, #valid?, #valid_till, #revokers, #primary_fingerprint, #protection_type/mode/cipher/hash/iterations.
  • Misc utilities: Rnp.backend_string/backend_version, Rnp#request_password(context, key:) (buffer securely cleared), Generate#request_password=, Encrypt#flags= (NOWRAP), Output.to_armor + #armor_line_length= + #finish, Rnp.dump_packets.

Verification (macOS arm64)

  • Against librnp v0.18.1: 561 examples, 0 failures; against librnp main: 561 examples, 0 failures (+79 new examples on both)

Remaining (23)

  • Deliberate skips: rnp_input_from_stdin, rnp_output_to_stdout, rnp_key_25519_bits_tweak(ed).
  • Deprioritized follow-ups: uid introspection (rnp_uid_get_*, rnp_uid_remove), rnp_key_revoke/set_expiration/get_default_key/export_autocrypt/export_revocation, op file name/mtime setters, rnp_op_encrypt_set_aead_bits, rnp_op_verify_get_protection_info, rnp_output_pipe/to_file.

ronaldtse added 14 commits July 20, 2026 01:34
Attach (with optional-symbol probing) the librnp functions needed for
verification/signature introspection, key certification, security
profile rules, key inspection and misc utilities, along with the
related constants.
Verify op: flags= (RNP_VERIFY_*), format (literal data format),
recipient and symenc enumeration (recipients, used_recipient, symencs,
used_symenc) via the new Recipient and Symenc value objects.

Signature: signature_type, fingerprint, expiration_time, features,
key_flags, key_expiration, primary_uid?, key_server(+prefs), revoker,
revocation_reason, trust_level, preferred_ciphers/hashes/compressions,
valid?, export, and the subpacket API (each_subpacket, subpackets,
subpacket) via the new Signature::Subpacket value object.
KeySignature wraps the editable key signature handle created by
Key#start_certification, Key#start_direct_signature and
Key#start_revocation_signature. It supports setting the hash, creation
time, key flags/expiration, features, preferred algorithms, primary
userid flag, key server (+preferences), revocation reason, designated
revoker and trust level, and is finalized with #sign.

Also Key#revocation_signature, Key#remove_signature (single) and
Key#remove_signatures (filtered by invalid/unknown-key/non-self
flags).
Rnp#add_security_rule, #security_rule and #remove_security_rule wrap
the librnp security rule API (0.16.3+), allowing to override the
default algorithm security levels (e.g. to accept SHA1 signatures).
Rnp#timestamp= wraps rnp_set_timestamp.
New Key methods: version, expired?, valid?, valid_till, revokers,
primary_fingerprint and the secret key protection information
(protection_type, protection_mode, protection_cipher, protection_hash,
protection_iterations).
- Rnp.backend_string and Rnp.backend_version (crypto backend info)
- Rnp#request_password (via the ffi password provider, securely
  clearing the buffer with rnp_buffer_clear)
- Generate#request_password= (request key protection password via the
  password provider)
- Encrypt#flags= (RNP_ENCRYPT_NOWRAP)
- Output.to_armor, Output#armor_line_length= and Output#finish
- Rnp.dump_packets (human-readable packet dump)
rnp_output_destroy() accesses the base output when destroying an
armored output (propagating the keep flag and writing the armor
trailer), so the armored output must be destroyed first. With two
independent AutoPointers the GC could finalize the base output first,
corrupting the heap (observed as glibc malloc aborts in CI, e.g. in
the GC-forcing z_spec).

Coordinate the two finalizers via a shared state so the armored
output is always destroyed before the base output, exactly once,
while keeping the base output usable on its own.
Sign, Verify and Encrypt operations did not reference their input and
output objects, so a caller creating an operation with temporary
Input/Output objects could have them garbage collected before
execute(), resulting in a use-after-free read (observed under
GC.stress).
The signature removal specs are set up via the key certification API
(0.18+), but their inner 'skip: false' metadata (the removal API
itself exists in 0.17.x) overrode the outer skip and ran them anyway,
raising FeatureNotAvailableError. Drop the inner skip metadata.

Also, rnp_signature_is_valid() only learnt to return the validation
status of document signatures and to revalidate key signatures
(RNP_SIGNATURE_REVALIDATE) in 0.18.0. Gate the affected specs on the
new 'signature-validity-status' feature.
Attach the last 23 exported-but-unbound librnp functions, completing
FFI parity: the uid API, key lifecycle (revoke, expiration, default
key, autocrypt/revocation export), op file metadata and protection
info, I/O stream helpers and Curve25519 bit tweaking, along with the
related constants. Symbols newer than the minimum supported librnp
(0.15.2) - the stdio helpers and 25519 tweaking - go through the
optional-symbol probing.
Extend the UserID object with data, type, primary?, valid?,
revocation_signature and remove (wraps rnp_uid_remove). The uid
handle references the owning key internally, so the key is now
retained by the UserID to prevent use-after-free if the key object
is garbage collected first.
- Key#revoke (with optional hash, reason code and text)
- Key#set_expiration
- Key#default_key (usage-based default key/subkey selection)
- Key#export_autocrypt (minimal Autocrypt export, binary or base64)
- Key#export_revocation (standalone revocation signature export)
- Key#x25519_bits_tweak / #x25519_bits_tweaked? (advanced Curve25519
  secret key bit tweaking; the latter returns true when the bits are
  correctly set)
- Sign#file_name= / #file_mtime= and Encrypt#file_name= /
  #file_mtime= (embedded into the literal data packet)
- Encrypt#aead_bits= (AEAD chunk size bits)
- Verify#protection_info (encryption mode, cipher and integrity
  status of the processed message)
- Verify#file_info (embedded file name and modification time)
- Output.to_file (with overwrite and atomic random-name options)
- Output#pipe_from (copy an input to the output)
- Input.from_stdin and Output.to_stdout (convenience-only wrappers,
  feature-gated)
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.

1 participant