Pular para o conteúdo principal

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​

ArtifactFromHow you consume it
crosspki.dllZIP, bin/x64/ and bin/x86/Windows: deployed next to your executable, loaded at runtime
libcrosspki.dylibZIP, bin/macos/macOS: deployed in Contents/Frameworks/, loaded at runtime
crosspki_cef_bridge.h / .cppZIPcompiled into your application
crosspki-cefnpm, or js/ in the ZIPimported by your web code
crosspki.hZIPoptional — 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.dylib is 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++20
    93.1.14yesno
    110.0.32yesyes
    111.2.7yesyes
    138.0.62yesyes
    149.0.6noyes

    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-builtins with 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​

  1. Compile bridge/crosspki_cef_bridge.cpp into your host — the same target that builds your CefApp and CefClient.
  2. 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 setupWhat resolves
Angular, webpack, vite, rollup, esbuildimport { CrossPki } from 'crosspki-cef'
Native ES modules, no bundlercrosspki-cef/standalone
No build step at alldist/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​

SymptomCauseFix
Every call rejects with platformNotSupported, "native bridge not found"The renderer half is not installedForward OnContextCreated to RendererBridge (section 5)
Calls never settle — the promise neither resolves nor rejectsA message is being dropped in one directionForward 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 loadedWindows: 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 IDsLibrary validation: a hardened-runtime app refused a library signed by another teamRe-sign libcrosspki.dylib with your identity (section 8)
macOS: a dialog asks whether your app may use a key in the keychainThe key belongs to another application — installed with Keychain Access, or by another programExpected on macOS; "Always Allow" remembers the answer. Keys your application imported or generated itself do not ask
TypeError: window.crossPkiQuery is not a functionYour web code ran before the renderer bridge installed the entry pointCall CrossPKI after page load, not during early script evaluation
A certificate visible in Windows or Keychain Access does not appear in the listIts 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 excludedExpected. Only the user's certificates with a usable key are listed
Signing appears to hangThe provider's smartcard PIN dialog opened behind your application windowBring it forward from the taskbar. It has no owner window, so it can be painted behind the app
algorithmNotSupported when using SHA-224Windows CNG has no SHA-224 implementation, and macOS matches WindowsUse SHA-256 or stronger
certificateNotFound for a thumbprint that existsThe thumbprint is Base64 of the SHA-256 of the DER bytes, not a hex SHA-1 fingerprintUse the thumbprint value from listCertificatesWithKey() verbatim
wrongPassword importing a valid PKCS#12The content is not valid PKCS#12, or the password is wrongConfirm the file is a .pfx/.p12 and that it is Base64-encoded before being passed in
ERR_MODULE_NOT_FOUND importing the packageAn ESM loader that does not follow the exports mapImport 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 acceptsCompile 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.

View as Markdown