Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ members = [
"crates/perry-ext-fastify",
"crates/perry-ext-pdf",
"crates/perry-ext-ads",
"crates/perry-ext-node-forge",
"crates/perry-wasm-host",
"crates/perry-container-compose",
"crates/perry-container-e2e",
Expand Down Expand Up @@ -464,6 +465,7 @@ perry-ext-streams = { path = "crates/perry-ext-streams" }
perry-ext-fastify = { path = "crates/perry-ext-fastify" }
perry-ext-pdf = { path = "crates/perry-ext-pdf" }
perry-ext-ads = { path = "crates/perry-ext-ads" }
perry-ext-node-forge = { path = "crates/perry-ext-node-forge" }
perry-stdlib = { path = "crates/perry-stdlib" }
perry-diagnostics = { path = "crates/perry-diagnostics" }
perry-ui-model = { path = "crates/perry-ui-model" }
Expand Down
1 change: 1 addition & 0 deletions changelog.d/7033-ext-node-forge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a native `node-forge` binding (`perry-ext-node-forge`) covering the PKI subset Socket Firewall's TLS-MITM CA uses — RSA `generateKeyPair`, the `createCertificate` builder (`setSubject`/`setIssuer`/`setExtensions`/`sign`), `certificateFromPem`/`certificateToPem`, `privateKeyFromPem`/`privateKeyToPem`/`publicKeyToPem`, and `md.sha256.create` — backed by RustCrypto (`rsa` + `x509-cert`) so apps stop AOT-compiling forge's pure-JS bignum/ASN.1 code. Certificates are signed `sha256WithRSAEncryption` and verify against real TLS clients (`openssl verify`).
5 changes: 5 additions & 0 deletions crates/perry-api-manifest/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ pub const NATIVE_MODULES: &[&str] = &[
// the one perry-runtime implementation — no N-API addon involved.
"node-pty",
"@lydell/node-pty", // API-identical node-pty fork (see above)
// #466: node-forge PKI subset (RSA keygen, X.509 build/sign, PEM).
// Bundled wrapper at `crates/perry-ext-node-forge`; served natively
// for Socket Firewall's TLS-MITM CA so forge's pure-JS crypto isn't
// AOT-compiled.
"node-forge",
];

/// Node built-in submodules that Perry routes through the
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-api-manifest/src/entries/part_2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,24 @@ pub(crate) const API_MANIFEST_PART_2: &[ApiEntry] = &[
TypeSpec::BigInt,
),
method("ethers", "createRandom", false, Some("Wallet")),
// node-forge PKI subset (perry-ext-node-forge). Declared with the
// arity-agnostic `method(...)` form (params &[], returns Any) so the
// #512 dispatch↔manifest drift gate is satisfied by name while the
// real argument shapes live in the wrapper. Namespaced call sites
// (`forge.pki.rsa.generateKeyPair`, `forge.md.sha256.create`)
// dispatch once perry-hir flattens the sub-namespace member chains.
method("node-forge", "generateKeyPair", false, None),
method("node-forge", "createCertificate", false, None),
method("node-forge", "certificateFromPem", false, None),
method("node-forge", "certificateToPem", false, None),
method("node-forge", "privateKeyFromPem", false, None),
method("node-forge", "privateKeyToPem", false, None),
method("node-forge", "publicKeyToPem", false, None),
method("node-forge", "create", false, None),
method("node-forge", "setSubject", true, Some("Certificate")),
method("node-forge", "setIssuer", true, Some("Certificate")),
method("node-forge", "setExtensions", true, Some("Certificate")),
method("node-forge", "sign", true, Some("Certificate")),
// ===========================================================
// Methods dispatched via custom Expr::* variants
// (perry-hir/src/lower/expr_call.rs and expr_member.rs)
Expand Down
122 changes: 122 additions & 0 deletions crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,126 @@ pub(super) const UTILS_CRYPTO_ROWS: &[NativeModSig] = &[
args: &[NA_STR, NA_STR],
ret: NR_PTR,
},
// ========== node-forge (PKI subset — perry-ext-node-forge) ==========
// Namespaced statics (`forge.pki.rsa.generateKeyPair`,
// `forge.pki.createCertificate`, `forge.md.sha256.create`, ...). These
// dispatch once perry-hir flattens the `forge.pki.*` / `forge.md.*`
// sub-namespace member chains to `NativeMethodCall { module:
// "node-forge", method }`. Object-returning fns box as NR_PTR (they
// return `JsValue::from_object_ptr`, which the double-tag-idempotent
// NR_PTR path leaves intact); PEM emitters return `*mut StringHeader`
// → NR_STR. Key/cert handles cross as NaN-boxed objects (NA_F64); PEM
// inputs as raw string pointers (NA_STR).
NativeModSig {
module: "node-forge",
has_receiver: false,
method: "generateKeyPair",
class_filter: None,
runtime: "js_node_forge_generate_key_pair",
args: &[NA_F64],
ret: NR_PTR,
},
NativeModSig {
module: "node-forge",
has_receiver: false,
method: "createCertificate",
class_filter: None,
runtime: "js_node_forge_create_certificate",
args: &[],
ret: NR_PTR,
},
NativeModSig {
module: "node-forge",
has_receiver: false,
method: "certificateFromPem",
class_filter: None,
runtime: "js_node_forge_certificate_from_pem",
args: &[NA_STR],
ret: NR_PTR,
},
NativeModSig {
module: "node-forge",
has_receiver: false,
method: "certificateToPem",
class_filter: None,
runtime: "js_node_forge_certificate_to_pem",
args: &[NA_F64],
ret: NR_STR,
},
NativeModSig {
module: "node-forge",
has_receiver: false,
method: "privateKeyFromPem",
class_filter: None,
runtime: "js_node_forge_private_key_from_pem",
args: &[NA_STR],
ret: NR_PTR,
},
NativeModSig {
module: "node-forge",
has_receiver: false,
method: "privateKeyToPem",
class_filter: None,
runtime: "js_node_forge_private_key_to_pem",
args: &[NA_F64],
ret: NR_STR,
},
NativeModSig {
module: "node-forge",
has_receiver: false,
method: "publicKeyToPem",
class_filter: None,
runtime: "js_node_forge_public_key_to_pem",
args: &[NA_F64],
ret: NR_STR,
},
// `forge.md.sha256.create()` → a marker digest object.
NativeModSig {
module: "node-forge",
has_receiver: false,
method: "create",
class_filter: None,
runtime: "js_node_forge_md_sha256_create",
args: &[],
ret: NR_PTR,
},
// Certificate builder instance methods. The receiver (the JS cert
// object) is NaN-unboxed to an `i64` `*mut ObjectHeader` and passed
// as the first arg; the FFI writes into fixed object slots.
NativeModSig {
module: "node-forge",
has_receiver: true,
method: "setSubject",
class_filter: Some("Certificate"),
runtime: "js_node_forge_cert_set_subject",
args: &[NA_F64],
ret: NR_VOID,
},
NativeModSig {
module: "node-forge",
has_receiver: true,
method: "setIssuer",
class_filter: Some("Certificate"),
runtime: "js_node_forge_cert_set_issuer",
args: &[NA_F64],
ret: NR_VOID,
},
NativeModSig {
module: "node-forge",
has_receiver: true,
method: "setExtensions",
class_filter: Some("Certificate"),
runtime: "js_node_forge_cert_set_extensions",
args: &[NA_F64],
ret: NR_VOID,
},
NativeModSig {
module: "node-forge",
has_receiver: true,
method: "sign",
class_filter: Some("Certificate"),
runtime: "js_node_forge_cert_sign",
args: &[NA_F64, NA_F64],
ret: NR_VOID,
},
];
50 changes: 50 additions & 0 deletions crates/perry-ext-node-forge/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[package]
name = "perry-ext-node-forge"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Native bindings for the npm `node-forge` package — the PKI subset (RSA keygen, X.509 certificate build/sign, PEM round-trips) that Socket Firewall's TLS-MITM CA uses. Uses only `perry-ffi` plus RustCrypto (`rsa` / `x509-cert`). CPU-only: no tokio, not in `binding_needs_shared_tokio`."

[lints]
workspace = true

[lib]
crate-type = ["staticlib", "rlib"]

[dependencies]
perry-ffi.workspace = true
perry-runtime = { workspace = true, features = ["default", "stdlib"] }
serde = { workspace = true }
serde_json = { workspace = true }
# RustCrypto PKI stack — all already present in the workspace Cargo.lock.
rsa = { version = "0.9", features = ["sha2", "pem"] }
sha2 = "0.10"
x509-cert = { version = "0.2", features = ["builder", "hazmat"] }
der = { version = "0.7", features = ["oid"] }
spki = "0.7"
const-oid = "0.9"
pem = "3"
rand = "0.8"
time = { version = "0.3", features = ["parsing"] }

[dev-dependencies]
perry-ffi = { workspace = true, features = ["runtime-link"] }
# #6303: perry-runtime MUST be built here with the same feature set the shipped
# `libperry_runtime.a` / `libperry_stdlib.a` carry (i.e. its `default`). This crate
# is a `staticlib`, so it BUNDLES the perry-runtime rlib objects into
# `libperry_ext_*.a` — and perry links the ext archives BEFORE stdlib/runtime
# (`prefer_well_known_before_stdlib`), so those bundled objects WIN the link for
# every symbol they define. The workspace dep is `default-features = false`, so
# without `"default"` here a per-crate `cargo build -p perry-ext-<x>` (exactly what
# release-packages.yml does in its per-crate loop) bundles a runtime with
# `regex-engine`/`temporal`/... compiled OUT. The dispatchers those features gate
# are exported UNCONDITIONALLY (`js_string_replace_search_dyn`,
# `js_native_call_method`, ...) with the feature-gated logic `#[cfg]`-ed out of the
# BODY — so the degraded copy silently ToString-coerces a RegExp argument and
# searches for it literally instead of matching it (str.replace(re, fn) never fires
# its callback). Keep `"default"` in lock-step with perry-runtime's default feature
# list; the `ext_crates_bundle_a_full_featured_perry_runtime` test (well_known.rs) guards it.
# `stdlib`: this crate bundles perry-runtime into its staticlib and is co-linked
# with the real perry-stdlib, so drop the bundled no-op stdlib_stubs that would
# otherwise shadow perry-stdlib's real symbols (#6314). `default`: keep the copy
# feature-identical to the shipped runtime so gated dispatchers behave (#6303).
Loading
Loading