Skip to content
Open
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
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