Skip to content

feat(security): Phase 2 audit memory-safety sweep#984

Merged
boorad merged 3 commits intomainfrom
feat/security-audit-phase-2
Apr 27, 2026
Merged

feat(security): Phase 2 audit memory-safety sweep#984
boorad merged 3 commits intomainfrom
feat/security-audit-phase-2

Conversation

@boorad
Copy link
Copy Markdown
Collaborator

@boorad boorad commented Apr 27, 2026

Summary

Closes Phase 2 of plans/todo/security-audit.md across 24 C++ files (+327/-480 net — RAII actually shrinks the codebase by collapsing manual error-path frees). All five Phase 2 items shipped together.

# Item Modules touched
2.1 Raw new uint8_t[]std::unique_ptr<uint8_t[]> Hash, HMAC, KMAC, BLAKE3, PBKDF2, Scrypt, HKDF, all cipher update()/final(), RSA-cipher decrypt sentinels, Ed25519 (6 sites), ML-DSA (3 sites), ML-KEM (4 sites)
2.2 Raw EVP_PKEY* → smart pointers (DSA template) RSA, EC, Ed25519 keypairs
2.3 Promise<…>::async([this, …])shared_cast + [self, …] ML-DSA (3 sites), ML-KEM (3 sites). DH had no async sites despite the audit listing.
2.4 Eliminate EVP_PKEY_CTX_new_from_name pre-creation Sign/Verify handles, ML-DSA, Ed25519
2.5 Thread-unsafe ERR_error_string(.., NULL)getOpenSSLError() Ed25519 (2 sites)

Notable findings

  • Ed25519 import-helper leak (2.2): importPublicKey/importPrivateKey previously returned raw EVP_PKEY* that callers never freed at HybridEdKeyPair.cpp:155,221 (audit HIGH). Now return owning EVP_PKEY_ptr; the borrow-the-instance-key path uses EVP_PKEY_up_ref so refcounts stay balanced.
  • Bonus leak fix (2.4): crypto-specialist review confirmed the old EVP_PKEY_CTX_new_from_name wasn't just at risk of double-free — OpenSSL was silently leaking it on the success path, since EVP_DigestSignInit allocates a fresh PKEY_CTX from the key's keymgmt and overwrites the input pointer. The new code (passing nullptr) closes both the audited double-free and this previously-unreported leak. Matches ncrypto::EVPMDCtxPointer::signInit.
  • PQ async UAF (2.3): HybridObject already inherits from std::enable_shared_from_this<HybridObject>, so this->shared_cast<…>() works directly inside Promise::async lambdas. Capturing self by value closes the use-after-free risk if the JS object is GC'd while async is in flight.

Defense-in-depth

secureZero added on Scrypt/HKDF error paths and on Ed25519/ML-DSA/ML-KEM getPrivateKey BIO buffers.

Crypto-specialist review

Approved all four substantive concerns:

  1. Algorithm selection unchanged after dropping EVP_PKEY_CTX_new_from_name (verified against OpenSSL 3.x crypto/evp/m_sigver.c::do_sigver_init).
  2. EVP_PKEY_up_ref refcount semantics correct and thread-safe.
  3. BIO secureZero is safe redundancy with BUF_MEM_free's OPENSSL_clear_free.
  4. release() + make_shared window matches Nitro's own ArrayBuffer::wrap convention.

Phase 1 doc cleanup

Phase 1 (PR #983) shipped without updating its checklist in security-audit.md. This PR backfills the [x] marks and adds a Progress Log entry for it.

Test plan

  • Pre-commit hook passed: lint-staged, clang-format --dry-run --Werror, tsc --noEmit (both packages), bob build.
  • No behavior changes — every refactor preserves exception messages and observable semantics.
  • Run the React Native example app's existing suites (Hash, HMAC, KMAC, BLAKE3, PBKDF2, Scrypt, HKDF, all ciphers, RSA, EC, Ed25519, ML-DSA, ML-KEM, Sign/Verify) — no new tests added in this PR (Phase 4 will add the missing test vectors).
  • Spot-check a sign + verify round-trip with each key type (RSA, ECDSA, Ed25519, ML-DSA-44/65/87) since 2.4 changed the EVP_DigestSignInit invocation shape.
  • Spot-check ML-DSA / ML-KEM async generate/sign/verify/encapsulate/decapsulate under load (2.3 changed the async lambda capture).

boorad added 3 commits April 26, 2026 23:17
… EVP_PKEY*, EVP_PKEY_CTX, EVP_MD_CTX; shared_from_this in PQ async

Closes Phase 2 of plans/todo/security-audit.md across 24 C++ files
(+327/-480 net). All five Phase 2 items shipped together:

2.1  Raw `new uint8_t[]` → `std::unique_ptr<uint8_t[]>` + `release()` into
     `NativeArrayBuffer` in Hash, HMAC, KMAC, BLAKE3, PBKDF2, Scrypt, HKDF,
     the cipher base `update()`, ChaCha20/ChaCha20-Poly1305/XChaCha20-Poly1305/
     XSalsa20-Poly1305, CCM `final()`, RSA-cipher decrypt sentinels, Ed25519
     (6 sites), ML-DSA (3 sites), and ML-KEM (4 sites).

2.2  Raw `EVP_PKEY*` → `std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>`
     in RSA, EC, and Ed25519 keypairs (DSA pattern as template).
     `Ed25519::importPublicKey`/`importPrivateKey` now return owning
     `EVP_PKEY_ptr` and use `EVP_PKEY_up_ref` for the borrow-the-instance-key
     path, closing the audit-flagged leak at HybridEdKeyPair.cpp:155,221.

2.3  `Promise<…>::async([this, …])` → `auto self = this->shared_cast<…>();
     [self, …]` in ML-DSA (3 sites) and ML-KEM (3 sites). Closes the
     use-after-free risk if the JS object is GC'd while async is in flight.

2.4  Eliminate the unnecessary `EVP_PKEY_CTX_new_from_name` pre-creation in
     Sign/Verify handles, ML-DSA, and Ed25519 — pass `nullptr` for the
     `EVP_PKEY_CTX**` arg and let `EVP_DigestSignInit`/`EVP_DigestVerifyInit`
     allocate from the key's keymgmt (matches `ncrypto::EVPMDCtxPointer::
     signInit`). Crypto-specialist review confirmed the old code was actually
     *leaking* the pre-allocated PKEY_CTX (OpenSSL silently overwrote the
     pointer on success), so this fix closes both the audited double-free
     *and* an unreported leak. Wraps EVP_MD_CTX/EVP_PKEY_CTX in local
     `unique_ptr` aliases so all manual error-path frees collapse.

2.5  Replace Ed25519's two `ERR_error_string(ERR_get_error(), NULL)` calls
     (thread-unsafe static buffer) with the shared `getOpenSSLError()` helper.

Defense-in-depth: `secureZero` added on Scrypt/HKDF error paths and on
Ed25519/ML-DSA/ML-KEM `getPrivateKey` BIO buffers.

Crypto-specialist approved all four substantive concerns: algorithm
selection unchanged (verified against OpenSSL 3.x m_sigver.c::do_sigver_init),
refcount semantics correct, BIO secure-zero is safe redundancy with
`BUF_MEM_free`'s `OPENSSL_clear_free`, `release()` + `make_shared` window
matches Nitro's own `ArrayBuffer::wrap`.
The e2e-android-test.yml and e2e-ios-test.yml workflows referenced
'cpp/**', 'nitrogen/**', and 'src/**' at repo root — directories that
no longer exist after the workspace migration to
'packages/react-native-quick-crypto/'. Result: every C++-only PR
silently skipped both E2E suites (PR #982 Phase 0, PR #983 Phase 1,
and PR #984 Phase 2 all hit this).

Updates both pull_request and push path filters to point at the
workspace locations. Each workflow file is itself in its own paths
filter, so this commit triggers the workflows on PR #984 to run for
the first time on this branch's C++ changes.
…ion, gate push on local test run

Adds three Phase Execution Rules to the Implementation Plan:

1. Commit after each sub-section (single numbered task or wave), not at
   the end of the phase. Preserves bisectability and review-ability.
2. Do not push or open a PR until the user has run the example app's
   tests locally and reported pass/fail. Pre-commit hooks cover lint /
   format / tsc / bob build only — they cannot exercise the native
   bridge. Tests live in example/src/tests/ and require the RN
   example app environment.
3. Fix path-filtered CI workflows on the same branch when they fail to
   trigger. Workflow files are in their own paths filters, so the fix
   re-triggers the run.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

🤖 End-to-End Test Results - iOS

Status: ✅ Passed
Platform: iOS
Run: 24975100974

📸 Final Test Screenshot

Maestro Test Results - ios

Screenshot automatically captured from End-to-End tests and will expire in 30 days


This comment is automatically updated on each test run.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

🤖 End-to-End Test Results - Android

Status: ✅ Passed
Platform: Android
Run: 24975100954

📸 Final Test Screenshot

Maestro Test Results - android

Screenshot automatically captured from End-to-End tests and will expire in 30 days


This comment is automatically updated on each test run.

@boorad boorad self-assigned this Apr 27, 2026
@boorad boorad merged commit e1b306a into main Apr 27, 2026
7 of 8 checks passed
@boorad boorad deleted the feat/security-audit-phase-2 branch April 27, 2026 13:11
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