CrossPKI for CEF — Integration Guide
CrossPKI gives your Chromium application access to the digital certificates and private keys held in the user's Windows certificate store or macOS keychain, from JavaScript.
This guide assumes a working CEF host with your own CefApp and CefClient, on
Windows, macOS, or both. Integration is six forwarded callbacks, one source file
compiled into your host, one npm package, and one native library deployed with your
application — crosspki.dll beside your executable on Windows, libcrosspki.dylib
in your app bundle on macOS. The source you compile and the JavaScript you call are
the same on both.
1. What you get
| Artifact | From | How you consume it |
|---|---|---|
crosspki.dll | ZIP, bin/x64/ and bin/x86/ | Windows: deployed next to your executable, loaded at runtime |
libcrosspki.dylib | ZIP, bin/macos/ | macOS: deployed in Contents/Frameworks/, loaded at runtime |
crosspki_cef_bridge.h / .cpp | ZIP | compiled into your application |
crosspki-cef | npm, or js/ in the ZIP | imported by your web code |
crosspki.h | ZIP | optional — only to call the C API directly |
The bridge ships as source because it must compile against your own CEF SDK and toolset. Nothing is linked at build time — it loads the native library at runtime.
2. Requirements
-
Windows, x64 or x86. CrossPKI uses the Windows certificate store and CNG. The ZIP carries a DLL for each architecture; deploy the one matching your host process, since a process can only load a DLL of its own bitness.
-
macOS 10.15 or later on Intel, 11 or later on Apple Silicon. CrossPKI uses the user's keychain through Security.framework.
libcrosspki.dylibis one universal file for both architectures, so there is nothing to choose. -
CEF. The bridge is compiled as part of your application, so what matters is that it builds against your SDK. Verified with MSVC v143 against the current stable build of each line:
CEF /std:c++17/std:c++2093.1.14 yes no 110.0.32 yes yes 111.2.7 yes yes 138.0.62 yes yes 149.0.6 no yes The constraint is the language standard, not the bridge — the bridge builds under both. Each CEF line restricts which standard its own headers accept, and your application is already subject to that restriction: CEF 93 uses
std::result_of, removed in C++20, so a CEF 93 host must compile as C++17; CEF 149 uses C++20 concepts, so a CEF 149 host must compile as C++20; CEF 110 and 138 accept either.Compile the bridge with the same standard as the rest of your host. Versions outside this table are likely to work — a mismatch surfaces as a compile error during integration, never as a failure in production.
On macOS the bridge is verified with Xcode 26 against CEF 111.2.7, arm64 and x86_64, under CEF's own compiler flags (
-Werror -Wextra, no exceptions, no RTTI). CEF 111's headers themselves need-Wno-deprecated-builtinswith that Xcode; that affects any CEF 111 host, not only CrossPKI.The bridge also needs
libcef_dll_wrapper, which your application almost certainly already links. -
Node.js and npm, for your web build.
3. Download
https://cdn.lacunasoftware.com/crosspki/crosspki-cef-1.1.0.zip
https://cdn.lacunasoftware.com/crosspki/crosspki-cef-1.1.0.zip.sha256.txt
SHA-256: 4553a3edf683e3c8025e4971b692a66b030a5f47d5231894f18845075a6a1e6e
One ZIP serves both systems. Every release has an immutable URL, so a given version
always resolves to the same bytes. crosspki.dll is Authenticode-signed by Lacuna
Software, and libcrosspki.dylib is signed with Lacuna Software's Developer ID —
verify the checksum and the signature before deploying. If your environment
enforces WDAC or AppLocker, the Authenticode signature is what those policies
evaluate.
4. Add the bridge to your build
- Compile
bridge/crosspki_cef_bridge.cppinto your host — the same target that builds yourCefAppandCefClient. - Add the package root to that target's include directories, so
#include "bridge/crosspki_cef_bridge.h"resolves.
Your CEF SDK include directory stays as it is; the bridge includes CEF headers by
their usual include/... paths.
5. Wire the renderer process
The renderer half installs the JavaScript entry point. It does not load the DLL — no certificate or key code runs in a renderer.
#include "bridge/crosspki_cef_bridge.h"
class MyApp : public CefApp,
public CefRenderProcessHandler {
public:
CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() override {
return this;
}
void OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) override {
crosspki_.OnContextCreated(browser, frame, context);
}
void OnContextReleased(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) override {
crosspki_.OnContextReleased(browser, frame, context);
}
bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) override {
// Return false instead if you have your own messages to handle.
return crosspki_.OnProcessMessageReceived(browser, frame, source_process, message);
}
private:
crosspki::cef::RendererBridge crosspki_;
IMPLEMENT_REFCOUNTING(MyApp);
};
All three are required: OnProcessMessageReceived is what carries results back from
the browser process, so omitting it means calls never return.
6. Wire the browser process
The browser half owns the DLL and does the work. Construct it on the browser UI
thread, after CefInitialize has returned.
#include "bridge/crosspki_cef_bridge.h"
class MyClient : public CefClient,
public CefLifeSpanHandler,
public CefRequestHandler {
public:
MyClient() {
// A stable identifier for your application. Use reverse-DNS notation and keep
// it the same across releases.
crosspki_ = std::make_unique<crosspki::cef::BrowserBridge>("com.example.myapp");
}
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
CefRefPtr<CefRequestHandler> GetRequestHandler() override { return this; }
bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) override {
return crosspki_->OnProcessMessageReceived(browser, frame, source_process, message);
}
// Both of these cancel calls still in flight for the browser.
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override {
crosspki_->OnBeforeClose(browser);
}
// Before CEF 124 this callback takes only (browser, status).
void OnRenderProcessTerminated(CefRefPtr<CefBrowser> browser,
TerminationStatus status,
int error_code,
const CefString& error_string) override {
crosspki_->OnRenderProcessTerminated(browser);
}
private:
std::unique_ptr<crosspki::cef::BrowserBridge> crosspki_;
IMPLEMENT_REFCOUNTING(MyClient);
};
Each call runs on a CEF background thread, so a slow certificate store read or a smartcard provider's dialog cannot block your UI.
BrowserBridge::IsAvailable() reports whether the native library was found and loaded.
Check it at startup to surface a clear message instead of letting the first PKI call
fail.
7. Install the JavaScript library
npm install crosspki-cef
TypeScript definitions are included; no @types package is needed.
The package ships in three shapes, and your build picks the right one automatically:
| Your setup | What resolves |
|---|---|
| Angular, webpack, vite, rollup, esbuild | import { CrossPki } from 'crosspki-cef' |
| Native ES modules, no bundler | crosspki-cef/standalone |
| No build step at all | dist/crosspki.bundle.js in a <script> tag, then the CrossPkiLib global |
If you cannot use npm, the same files are in the ZIP under js/.
Angular note: node-forge is a CommonJS dependency, so ng build may warn about
optimization bailouts. Either add node-forge to allowedCommonJsDependencies in
angular.json, or import the /standalone entry, which has it inlined.
8. Deploy
Windows. Ship crosspki.dll next to your host executable, alongside libcef.dll
and the rest of your CEF binaries — bin/x64/ for a 64-bit host, bin/x86/ for a
32-bit one. The bridge looks there first and only then falls back to the standard
search path, which is what stops a stray copy elsewhere on the machine from being
picked up instead.
macOS. Copy bin/macos/libcrosspki.dylib into your app bundle's
Contents/Frameworks/, beside Chromium Embedded Framework.framework. Only the main
application needs it — the helper apps never load it, because CrossPKI runs in the
browser process. The bridge looks in Contents/Frameworks/ first, then beside the
executable in Contents/MacOS/, then through your executable's rpath; it never loads
the library by bare name, which would let the working directory supply one.
Then re-sign it with your own identity, as part of signing your bundle, and notarise the app as you already do:
codesign --force --sign "Developer ID Application: <you>" --options runtime \
--timestamp "MyApp.app/Contents/Frameworks/libcrosspki.dylib"
A hardened-runtime app only loads libraries signed by its own team (library
validation), so our signature is there to let you verify what you received, not to
be shipped as is. Tools that sign a whole bundle — Xcode, electron-osx-sign-style
scripts, codesign --deep — do this for you.
There is nothing to register, no COM component, no entitlement, and no installer step, on either system.
9. Verify the integration
Run this from your application's web context. It exercises the whole path — renderer, IPC, browser process, DLL — without touching the user's certificates:
import { CrossPki, CrossPkiError } from 'crosspki-cef';
async function checkCrossPki() {
try {
const version = await CrossPki.getVersion();
console.log(`CrossPKI is wired up. Native library version ${version}.`);
const certs = await CrossPki.listCertificatesWithKey();
console.log(`${certs.length} certificate(s) with a private key.`);
for (const cert of certs) {
console.log(` - ${cert.subjectDisplayName}, expires ${cert.validityEnd}`);
}
} catch (e) {
if (e instanceof CrossPkiError) {
console.error(`CrossPKI failed [${e.code}]: ${e.message}`);
} else {
console.error(e);
}
}
}
A machine with no certificates reports 0 — that is a correct result, not a
failure. Certificates whose private key is missing or unusable are not listed.
Every CrossPKI method returns a promise and rejects with a CrossPkiError carrying
a code. Branch on code, never on message text.
For the full API — signing, key generation, CSRs, PKCS#12 import, ICP-Brasil fields — see https://docs.lacunasoftware.com/articles/crosspki.
10. Sandbox
CrossPKI works with the Chromium sandbox enabled or disabled, and does not ask
you to change your process model either way. The native library loads in the browser
process, never in a renderer, so a sandboxed renderer is fully compatible — and so is
a host that runs with no_sandbox = true, which is how CrossPKI's own test harness
runs, on both systems.
This is about Chromium's sandbox. On macOS, running the whole application under the
App Sandbox (com.apple.security.app-sandbox, required for the Mac App Store) is a
different thing and has not been tested: it restricts keychain access.
If your application keeps the sandbox on, nothing here requires relaxing it. If it runs without one, nothing here requires adding it.
11. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Every call rejects with platformNotSupported, "native bridge not found" | The renderer half is not installed | Forward OnContextCreated to RendererBridge (section 5) |
| Calls never settle — the promise neither resolves nor rejects | A message is being dropped in one direction | Forward OnProcessMessageReceived in both the renderer handler and the browser client, and confirm a BrowserBridge was actually constructed |
Calls reject with internal, "native bridge failure (1)" | The native library was not found or could not be loaded | Windows: place the DLL next to your executable and confirm its bitness matches the host. macOS: place the dylib in Contents/Frameworks/ and confirm it is signed by your team (below). Check that IsAvailable() returns true |
macOS: the library is present but does not load; Console shows code signature … not valid for use in process or different Team IDs | Library validation: a hardened-runtime app refused a library signed by another team | Re-sign libcrosspki.dylib with your identity (section 8) |
| macOS: a dialog asks whether your app may use a key in the keychain | The key belongs to another application — installed with Keychain Access, or by another program | Expected on macOS; "Always Allow" remembers the answer. Keys your application imported or generated itself do not ask |
TypeError: window.crossPkiQuery is not a function | Your web code ran before the renderer bridge installed the entry point | Call CrossPKI after page load, not during early script evaluation |
| A certificate visible in Windows or Keychain Access does not appear in the list | Its private key is missing or unreachable — an orphaned entry, or a token that is not connected. On macOS, identities in the System keychain are also excluded | Expected. Only the user's certificates with a usable key are listed |
| Signing appears to hang | The provider's smartcard PIN dialog opened behind your application window | Bring it forward from the taskbar. It has no owner window, so it can be painted behind the app |
algorithmNotSupported when using SHA-224 | Windows CNG has no SHA-224 implementation, and macOS matches Windows | Use SHA-256 or stronger |
certificateNotFound for a thumbprint that exists | The thumbprint is Base64 of the SHA-256 of the DER bytes, not a hex SHA-1 fingerprint | Use the thumbprint value from listCertificatesWithKey() verbatim |
wrongPassword importing a valid PKCS#12 | The content is not valid PKCS#12, or the password is wrong | Confirm the file is a .pfx/.p12 and that it is Base64-encoded before being passed in |
ERR_MODULE_NOT_FOUND importing the package | An ESM loader that does not follow the exports map | Import crosspki-cef/standalone, or use the <script> bundle |
The bridge does not compile, with errors inside CEF's own headers (std::result_of, convertible_to) | The C++ standard does not match what your CEF version accepts | Compile the bridge with the same /std: setting as the rest of your host — see section 2 |
Support
Include your CrossPKI version (CrossPki.getVersion()), your CEF version, and your
Windows or macOS version when reporting an issue.