Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ __pycache__
/.flatpak/
/vendor

# Web extension (generated for local dev)
webext/add-on/manifest.json

# IDE
/.vscode/settings.json
.idea
24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,26 @@ Alternatively, you can build the project yourself using the instructions in

## How to use

Right now, there are two ways to use this service.
Right now, there are three ways to use this service.

### Experimental Firefox Add-On
### Experimental Browser Extension

There is an add-on that you can install in Firefox 140+ that allows you to test
`credentialsd` without a custom Firefox build. You can get the XPI from the
[releases page][release-page] for the corresponding version of
`credentialsd-webextension` package that you installed.
There is a browser extension that allows you to test `credentialsd` without a
custom browser build. It overrides `navigator.credentials.create()` and
`navigator.credentials.get()` to route WebAuthn requests through the
credentialsd D-Bus service.

Currently, this add-on only works for https://webauthn.io and
Two browsers are supported from a single unified codebase:

- **Firefox 140+** — Install the XPI from the [releases page][release-page] for
the corresponding version of `credentialsd-webextension` package that you
installed.
- **Edge/Chromium (Chrome 111+, Edge 111+)** — Load as an unpacked extension
from `webext/add-on/` using the Chromium manifest. See
[`webext/README.md`](/webext/README.md#for-development-edgechromium) for
setup instructions.

Currently, the extension only works for https://webauthn.io and
https://demo.yubico.com, but can be used to test various WebAuthn options and
hardware.

Expand Down
65 changes: 56 additions & 9 deletions webext/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
This is a web extension that allows browsers to connect to the D-Bus service
provided by this project. It can be used for testing.

Currently, this is written only for Firefox; there will be some slight API
tweaks required to make this work in Chrome.
Both Firefox and Edge/Chromium are supported from a unified codebase in `add-on/`
with browser-specific manifests:
- `manifest.firefox.json` — Firefox (MV3, requires Firefox 140+)
- `manifest.chromium.json` — Edge/Chromium (MV3, requires Chrome 111+ or Edge 111+)

This requires some setup to make it work:

Expand Down Expand Up @@ -48,19 +50,64 @@ couple of options:
4. Navigate to [https://webauthn.io]().
5. Run through the registration and creation process.

## For Development
## For Development (Firefox)

(Note: Paths are relative to root of this repository)

1. Copy `webext/app/credential_manager_shim.json` to `~/.mozilla/native-messaging-hosts/credential_manager_shim.json`.
1. Copy `webext/app/credential_manager_shim.json` to `~/.mozilla/native-messaging-hosts/xyz.iinuwa.credentialsd_helper.json`.
2. In `webext/app/credential_manager_shim.py`, point the `DBUS_DOC_FILE`
variable to the absolute path to
`doc/xyz.iinuwa.credentialsd.Credentials.xml`.
3. In the copied file, replace the `path` key with the absolute path to `webext/app/credential_manager_shim.py`
4. Open Firefox and go to `about:debugging`
5. Click "This Firefox" > Load Temporary Extension. Select `webext/add-on/manifest.json`
6. Build with `ninja -C ./build` and run the following binaries binary to start the D-Bus services.
4. Copy the Firefox manifest into place:
```shell
cp webext/add-on/manifest.firefox.json webext/add-on/manifest.json
```
5. Open Firefox and go to `about:debugging`
6. Click "This Firefox" > Load Temporary Extension. Select `webext/add-on/manifest.json`
7. Build with `ninja -C ./build` and run the D-Bus services:
- `GSCHEMA_SCHEMA_DIR=build/credentialsd-ui/data ./build/credentialsd-ui/target/debug/credentialsd-ui`
- `./build/credentialsd/target/debug/credentialsd`
7. Navigate to [https://webauthn.io]().
8. Run through the registration and creation process.
8. Navigate to [https://webauthn.io]().
9. Run through the registration and creation process.

## For Development (Edge/Chromium)

(Note: Paths are relative to root of this repository)

1. In `webext/app/credential_manager_shim.py`, point the `DBUS_DOC_FILE`
variable to the absolute path to
`doc/xyz.iinuwa.credentialsd.Credentials.xml`.
2. Copy the Chromium manifest into place (Edge/Chrome require `manifest.json`):
```shell
cp webext/add-on/manifest.chromium.json webext/add-on/manifest.json
```
3. Open Edge and go to `edge://extensions` (or `chrome://extensions` for Chrome).
4. Enable "Developer mode" (toggle in top right).
5. Click "Load unpacked" and select the `webext/add-on/` directory.
6. Note the extension ID shown on the extensions page (e.g., `abcdefghijklmnop...`).
7. Create the native messaging manifest:
```shell
# For Edge:
mkdir -p ~/.config/microsoft-edge/NativeMessagingHosts
# For Chrome:
# mkdir -p ~/.config/google-chrome/NativeMessagingHosts
# For Chromium:
# mkdir -p ~/.config/chromium/NativeMessagingHosts

cat > ~/.config/microsoft-edge/NativeMessagingHosts/xyz.iinuwa.credentialsd_helper.json << EOF
{
"name": "xyz.iinuwa.credentialsd_helper",
"description": "Helper for integrating browser with credentialsd project",
"path": "$(readlink -f webext/app/credential_manager_shim.py)",
"type": "stdio",
"allowed_origins": [ "chrome-extension://YOUR_EXTENSION_ID/" ]
}
EOF
```
Replace `YOUR_EXTENSION_ID` with the extension ID from step 6.
8. Build with `ninja -C ./build` and run the D-Bus services:
- `GSCHEMA_SCHEMA_DIR=build/credentialsd-ui/data ./build/credentialsd-ui/target/debug/credentialsd-ui`
- `./build/credentialsd/target/debug/credentialsd`
9. Navigate to [https://webauthn.io]().
10. Run through the registration and creation process.
138 changes: 38 additions & 100 deletions webext/add-on/background.js
Original file line number Diff line number Diff line change
@@ -1,125 +1,63 @@
/*
On startup, connect to the "credential_shim" app.
*/
/**
* Background script that bridges content script messages
* to the native messaging host.
*
* Works in both Firefox (background script) and Chromium (service worker).
* ArrayBuffer serialization is handled by the MAIN world content script,
* so this script simply forwards messages between content and native.
*/

const browserAPI = globalThis.browser || globalThis.chrome;

let contentPort;
let nativePort;

function connected(port) {
console.log("received connection from content script");

// initialize content port
console.log('[credentialsd] received connection from content script');
contentPort = port;
console.log(contentPort);

// Initialize native port
nativePort = browser.runtime.connectNative("xyz.iinuwa.credentialsd_helper");
console.debug(nativePort);
if (nativePort.error !== null) {
console.error(nativePort.error)
throw nativePort.error
// Connect to native messaging host
nativePort = browserAPI.runtime.connectNative('xyz.iinuwa.credentialsd_helper');

// Check for connection errors (browser-specific patterns)
const connectError = nativePort.error || browserAPI.runtime.lastError;
if (connectError) {
console.error('[credentialsd] native connect error:', connectError.message || connectError);
return;
}
console.log(`connected to native app`)
console.log(nativePort)

// Set up content port listener
contentPort.onMessage.addListener(rcvFromContent)
console.log('[credentialsd] connected to native app');

// Set up native port listener
console.log("setting up native port response listener")
nativePort.onMessage.addListener(rcvFromNative);
contentPort.onMessage.addListener(rcvFromContent);
nativePort.onMessage.addListener(rcvFromNative);

nativePort.onDisconnect.addListener(() => {
const error = browserAPI.runtime.lastError;
if (error) {
console.error('[credentialsd] native port disconnected:', error.message);
}
});
}

function rcvFromContent(msg) {
const { requestId, cmd, options } = msg;
const origin = contentPort.sender.origin
const topOrigin = new URL(contentPort.sender.tab.url).origin
// const isCrossOrigin = origin === topOrigin
// const isTopLevel = contentPort.sender.frameId === 0;
const origin = contentPort.sender.origin;
const topOrigin = new URL(contentPort.sender.tab.url).origin;

if (options) {
const serializedOptions = serializeRequest(options)

console.debug(options.publicKey.challenge)
console.debug("background script received options, passing onto native app")
nativePort.postMessage({ requestId, cmd, options: serializedOptions, origin, topOrigin })
console.debug('[credentialsd] forwarding', cmd, 'to native app');
nativePort.postMessage({ requestId, cmd, options, origin, topOrigin });
} else {
console.debug("background script received message without arguments, passing onto native app")
nativePort.postMessage({ requestId, cmd, origin, topOrigin })
console.debug('[credentialsd] forwarding', cmd, '(no options) to native app');
nativePort.postMessage({ requestId, cmd, origin, topOrigin });
}
}

function rcvFromNative(msg) {
console.log("Received (native -> background): " + msg);
console.log("forwarding to content script");
const { requestId, data, error } = msg;
console.log('[credentialsd] received from native, forwarding to content');
contentPort.postMessage(msg);
}

function serializeBytes(buffer) {
const options = {alphabet: "base64url", omitPadding: true};
return new Uint8Array(buffer).toBase64(options)
}

function deserializeBytes(base64str) {
const options = {alphabet: "base64url"}
return Uint8Array.fromBase64(base64str, options)
}

function serializeRequest(options) {
// Serialize ArrayBuffers
const clone = structuredClone(options)
clone.publicKey.challenge = serializeBytes(clone.publicKey.challenge)
if (clone.publicKey.user) {
clone.publicKey.user.id = serializeBytes(clone.publicKey.user.id)
}
if (clone.publicKey.excludeCredentials) {
for (const cred of clone.publicKey.excludeCredentials) {
cred.id = serializeBytes(cred.id)
}
}
if (clone.publicKey.allowCredentials) {
for (const cred of clone.publicKey.allowCredentials) {
cred.id = serializeBytes(cred.id);
}
}
if (clone.publicKey.extensions && clone.publicKey.extensions.prf) {
if (clone.publicKey.extensions.prf.eval) {
clone.publicKey.extensions.prf.eval.first = serializeBytes(clone.publicKey.extensions.prf.eval.first);
if (clone.publicKey.extensions.prf.eval.second) {
clone.publicKey.extensions.prf.eval.second = serializeBytes(clone.publicKey.extensions.prf.eval.second);
}
}
if (clone.publicKey.extensions.prf.evalByCredential) {
const evalByCredential = clone.publicKey.extensions.prf.evalByCredential;

// Iterate over all credentialIDs, serialize the first/second bytebuffer and replace the original evalByCredential map
const result = {};
for (const credId in evalByCredentialData) {
const prfValue = evalByCredentialData[credId];

if (prfValue && prfValue.first) {
const newPrfValue = {
first: serializeBytes(prfValue.first)
};

if (prfValue.second) {
newPrfValue.second = serializeBytes(prfValue.second);
}
result[credId] = newPrfValue;
};
}
clone.publicKey.extensions.prf.evalByCredential = result;
}

if (clone.publicKey.extensions && clone.publicKey.extensions.credBlob) {
clone.publicKey.extensions.credBlob = serializeBytes(clone.publicKey.extensions.credBlob);
}
}
return clone
}


// Listen for connections from content script
console.log("Starting up credential_manager_shim background script")
browser.runtime.onConnect.addListener(connected);
console.log('[credentialsd] background script starting');
browserAPI.runtime.onConnect.addListener(connected);
36 changes: 36 additions & 0 deletions webext/add-on/content-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Content script running in ISOLATED world.
* Bridges window.postMessage from the MAIN world content script
* to the background script via runtime.connect.
*
* Works in both Firefox and Chromium browsers.
*/

const browserAPI = globalThis.browser || globalThis.chrome;
const port = browserAPI.runtime.connect({ name: 'credentialsd-helper' });

// Forward responses from background back to page context
port.onMessage.addListener((msg) => {
const { requestId, data, error } = msg;
window.postMessage({
type: 'credentialsd-response',
requestId,
data,
error,
}, '*');
});

port.onDisconnect.addListener(() => {
console.warn('[credentialsd] background port disconnected');
});

// Listen for requests from the MAIN world content script
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data?.type !== 'credentialsd-request') return;

const { requestId, cmd, options } = event.data;
port.postMessage({ requestId, cmd, options });
});

console.log('[credentialsd] content bridge active');
Loading