Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f08c72f
feat: add Edge/Chromium web extension port
phelix001 Feb 16, 2026
c40bce0
refactor: merge Firefox and Chromium add-ons into unified folder
phelix001 Feb 20, 2026
58d581e
docs: update READMEs for unified browser extension
phelix001 Feb 24, 2026
6ba8ab7
webext: address review feedback on manifest and meson build
phelix001 Mar 11, 2026
93d7eca
webext: use service_worker background in Firefox manifest
phelix001 Mar 12, 2026
70ba6f5
po: Add Georgian language support
EkaterinePapava Apr 20, 2026
08a3c7e
po: Add Georgian language support
EkaterinePapava Apr 20, 2026
37f1ea6
Merge pull request #147 from EkaterinePapava/main
iinuwa Apr 21, 2026
9e1e2dd
Include devices in initial request
iinuwa Feb 21, 2026
a1029d6
Merge pull request #149 from linux-credentials/initial-devices
iinuwa Apr 22, 2026
8e14fd7
Merge pull request #138 from phelix001/feat/edge-chromium-webext
iinuwa Apr 22, 2026
b602c49
Move RequestId to credentialsd_common::model
iinuwa Feb 21, 2026
cf50369
Make ViewRequest Clone
iinuwa Feb 21, 2026
094430c
Add FlowControlClient.
iinuwa Feb 21, 2026
040b3d3
Inject flow control client at request time instead of at startup.
iinuwa Feb 21, 2026
adbfd89
wip: Introduce FlowObject pattern
iinuwa Feb 21, 2026
c394020
wip: Serialize BackendRequest, reorder generics on CredentialService,…
iinuwa Feb 24, 2026
f0971dd
wip: transfer responsibility to talk to UI Control service to Flow Co…
iinuwa Feb 26, 2026
2fb4144
wip: daemon: move UiController out of CredentialService
iinuwa Feb 28, 2026
f133a76
wip: make CredentialService async trait
iinuwa Apr 23, 2026
f08f8d3
wip: clean up flow control stuff
iinuwa Apr 23, 2026
da36a20
daemon: Improve logging of trusted caller checks
iinuwa Apr 23, 2026
cc7c720
squash
iinuwa Apr 23, 2026
5ce707d
wip: ui: Receive events from frontend
iinuwa Apr 23, 2026
31aa360
ui: start portal impl
iinuwa Apr 23, 2026
a8541e4
abandon: add xdg-desktop-portal build as trusted caller
iinuwa Apr 23, 2026
1ea199a
serve credential portal backend and credentialsd-ui simultaneously
iinuwa Apr 23, 2026
4ddf577
squash to trusted callers
iinuwa Apr 23, 2026
a56b19c
ui: change name of impl portal interface
iinuwa Apr 23, 2026
bdef848
ui: send events to frontend
iinuwa Apr 23, 2026
6075cb9
It works!!!
iinuwa Apr 23, 2026
c63fe4b
Export docs
iinuwa Apr 23, 2026
3c3bb9a
wip: move backend request to top-level parameters
iinuwa Apr 23, 2026
846b582
wip: start flattening BackgroundEvent and BackendRequest
iinuwa Apr 24, 2026
e75a70a
ui: update docs
iinuwa Apr 24, 2026
128e494
ui: add TODO to clean up request objects
iinuwa Apr 24, 2026
77543e0
ui: Add portal configuration file
iinuwa Apr 25, 2026
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
6 changes: 3 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"program": "${workspaceFolder}/build/credentialsd/src/credentialsd",
"args": [],
"env": {
"RUST_LOG": "credentialsd=debug,libwebauthn=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug,libwebauthn::transport::channel=debug,zbus::object_server::debug,zbus=debug",
"CREDSD_TRUSTED_CALLERS": "/usr/bin/python3.14",
"RUST_LOG": "credentialsd=trace,libwebauthn=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug,libwebauthn::transport::channel=debug,zbus::object_server::debug,zbus=debug",
"CREDSD_TRUSTED_CALLERS": "/usr/bin/python3.14,/home/isaiah/Development/portal/xdg-desktop-portal/build/src/xdg-desktop-portal",
"CREDSD_TRUSTED_APP_IDS": "app:xyz.iinuwa.credentialsd.DemoCredentialsUi",
},
"sourceLanguages": [
Expand All @@ -29,7 +29,7 @@
"args": [],
"env": {
"GSETTINGS_SCHEMA_DIR": "${workspaceFolder}/build/credentialsd-ui/data",
"RUST_LOG": "credentialsd_ui=debug,zbus::trace,zbus::object_server::debug"
"RUST_LOG": "credentialsd_ui=trace,zbus::trace,zbus::object_server::debug"
},
"sourceLanguages": [
"rust"
Expand Down
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
5 changes: 1 addition & 4 deletions credentialsd-common/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ use std::pin::Pin;

use futures_lite::Stream;

use crate::{
model::{BackgroundEvent, Device},
server::RequestId,
};
use crate::model::{BackgroundEvent, Device, RequestId};

/// Used for communication from trusted UI to credential service
pub trait FlowController {
Expand Down
51 changes: 49 additions & 2 deletions credentialsd-common/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,17 @@ pub struct Device {

#[derive(Clone, Debug, Serialize, Deserialize, Type)]
pub enum Operation {
Create,
Get,
PublicKeyCreate,
PublicKeyGet,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
pub struct PortalBackendOptions {
/// Top-level origin of the request if different from the origin.
pub top_origin: Optional<String>,

/// RP ID of the request. Required for WebAuthn/PublicKey requests.
pub rp_id: Optional<String>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
Expand Down Expand Up @@ -122,6 +131,9 @@ pub struct RequestingParty {
pub origin: String,
}

/// Identifier for a request to be used for cancellation.
pub type RequestId = u32;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ViewUpdate {
SetTitle((String, String)),
Expand Down Expand Up @@ -252,6 +264,41 @@ pub enum NfcState {
Failed(Error),
}

pub enum BackendRequest {
/// Start Hybrid discovery
StartHybridDiscovery,

/// Start NFC discovery
StartNfcDiscovery,

/// Start USB discovery
StartUsbDiscovery,

/// Send client PIN
EnterClientPin(String),

/// Select a credential by credential ID
SelectCredential(String),

CancelRequest,
}

impl std::fmt::Debug for BackendRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::StartHybridDiscovery => write!(f, "StartHybridDiscovery"),
Self::StartNfcDiscovery => write!(f, "StartNfcDiscovery"),
Self::StartUsbDiscovery => write!(f, "StartUsbDiscovery"),
Self::EnterClientPin(_) => f
.debug_tuple("EnterClientPin")
.field(&"******".to_string())
.finish(),
Self::SelectCredential(arg0) => f.debug_tuple("SelectCredential").field(arg0).finish(),
Self::CancelRequest => write!(f, "CancelRequest"),
}
}
}

#[derive(Clone, Debug)]
pub enum BackgroundEvent {
UsbStateChanged(UsbState),
Expand Down
149 changes: 142 additions & 7 deletions credentialsd-common/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,144 @@ use serde::{
};
use zvariant::{
self, Array, DeserializeDict, DynamicDeserialize, NoneValue, Optional, OwnedValue,
SerializeDict, Signature, Structure, StructureBuilder, Type, Value, signature::Fields,
SerializeDict, Signature, Str, Structure, StructureBuilder, Type, Value, signature::Fields,
};

use crate::model::{BackgroundEvent, Operation, RequestingApplication};
use crate::model::{
BackendRequest, BackgroundEvent, Device, Operation, RequestId, RequestingApplication,
};

const TAG_VALUE_SIGNATURE: &Signature = &Signature::Structure(Fields::Static {
fields: &[&Signature::U8, &Signature::Variant],
});

/// Ceremony completed successfully
const BACKGROUND_EVENT_CEREMONY_COMPLETED: u32 = 0x01;
/// Device needs the client PIN to be entered. The backend should collect the
/// PIN and send it back with `EnterClientPin` event of `UserInteracted` signal.
const BACKGROUND_EVENT_NEEDS_PIN: u32 = 0x10;
const BACKGROUND_EVENT_NEEDS_USER_VERIFICATION: u32 = 0x11;
const BACKGROUND_EVENT_NEEDS_PRESENCE: u32 = 0x12;
const BACKGROUND_EVENT_SELECTING_CREDENTIAL: u32 = 0x13;

const BACKGROUND_EVENT_HYBRID_IDLE: u32 = 0x20;
const BACKGROUND_EVENT_HYBRID_STARTED: u32 = 0x21;
const BACKGROUND_EVENT_HYBRID_CONNECTING: u32 = 0x22;
const BACKGROUND_EVENT_HYBRID_CONNECTED: u32 = 0x23;

const BACKGROUND_EVENT_NFC_IDLE: u32 = 0x30;
const BACKGROUND_EVENT_NFC_WAITING: u32 = 0x31;
const BACKGROUND_EVENT_NFC_SELECTING_DEVICE: u32 = 0x32;
const BACKGROUND_EVENT_NFC_CONNECTED: u32 = 0x33;

const BACKGROUND_EVENT_USB_IDLE: u32 = 0x41;
const BACKGROUND_EVENT_USB_WAITING: u32 = 0x42;
const BACKGROUND_EVENT_USB_SELECTING_DEVICE: u32 = 0x43;
const BACKGROUND_EVENT_USB_CONNECTED: u32 = 0x44;

const BACKGROUND_EVENT_ERROR_AUTHENTICATOR: u32 = 0x80000001;
const BACKGROUND_EVENT_ERROR_NO_CREDENTIALS: u32 = 0x80000002;
const BACKGROUND_EVENT_ERROR_PIN_ATTEMPTS_EXHAUSTED: u32 = 0x80000003;
const BACKGROUND_EVENT_ERROR_INTERNAL: u32 = 0x80000004;
const BACKGROUND_EVENT_ERROR_TIMED_OUT: u32 = 0x80000005;
const BACKGROUND_EVENT_ERROR_CANCELLED: u32 = 0x80000006;

// BackendRequest
const BACKEND_REQUEST_START_HYBRID_DISCOVERY: u32 = 0x01;
const BACKEND_REQUEST_START_USB_DISCOVERY: u32 = 0x02;
const BACKEND_REQUEST_START_NFC_DISCOVERY: u32 = 0x03;
const BACKEND_REQUEST_ENTER_CLIENT_PIN: u32 = 0x04;
const BACKEND_REQUEST_SELECT_CREDENTIAL: u32 = 0x05;
const BACKEND_REQUEST_CANCEL_REQUEST: u32 = 0x06;

impl Type for BackendRequest {
const SIGNATURE: &'static Signature = TAG_VALUE_SIGNATURE;
}

impl From<&BackendRequest> for Structure<'_> {
fn from(value: &BackendRequest) -> Self {
match value {
BackendRequest::StartHybridDiscovery => tag_value_to_struct(0x01, None),
BackendRequest::StartNfcDiscovery => tag_value_to_struct(0x02, None),
BackendRequest::StartUsbDiscovery => tag_value_to_struct(0x03, None),
BackendRequest::EnterClientPin(pin) => {
tag_value_to_struct(0x04, Some(Value::Str(pin.into())))
}
BackendRequest::SelectCredential(credential_id) => {
tag_value_to_struct(0x05, Some(Value::Str(credential_id.into())))
}
BackendRequest::CancelRequest => tag_value_to_struct(0x06, None),
}
}
}

impl TryFrom<&Structure<'_>> for BackendRequest {
type Error = zvariant::Error;

fn try_from(value: &Structure<'_>) -> Result<Self, Self::Error> {
let (tag, value) = parse_tag_value_struct(value)?;

match tag {
0x01 => Ok(BackendRequest::StartHybridDiscovery),
0x02 => Ok(BackendRequest::StartNfcDiscovery),
0x03 => Ok(BackendRequest::StartUsbDiscovery),
0x04 => {
let s: Str = value.downcast_ref()?;
if s.is_empty() {
return Err(zvariant::Error::invalid_length(
s.len(),
&"a non-empty string",
));
}
Ok(BackendRequest::EnterClientPin(s.as_str().to_string()))
}
0x05 => {
let s: Str = value.downcast_ref()?;
if s.is_empty() {
return Err(zvariant::Error::invalid_length(
s.len(),
&"a non-empty string",
));
}
Ok(BackendRequest::SelectCredential(s.as_str().to_string()))
}
0x06 => Ok(BackendRequest::CancelRequest),
_ => Err(zvariant::Error::Message(format!(
"Unknown BackendRequest tag : {tag}"
))),
}
}
}

impl Serialize for BackendRequest {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let structure: Structure = self.into();
structure.serialize(serializer)
}
}

impl<'de> Deserialize<'de> for BackendRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let d = Structure::deserializer_for_signature(TAG_VALUE_SIGNATURE).map_err(|err| {
D::Error::custom(format!(
"could not create deserializer for tag-value struct: {err}"
))
})?;
let structure = d.deserialize(deserializer)?;
(&structure).try_into().map_err(|err| {
D::Error::custom(format!(
"could not deserialize structure into BackendRequest: {err}"
))
})
}
}

impl Type for BackgroundEvent {
const SIGNATURE: &'static Signature = TAG_VALUE_SIGNATURE;
}
Expand Down Expand Up @@ -308,9 +437,6 @@ impl Type for crate::model::HybridState {
const SIGNATURE: &'static Signature = TAG_VALUE_SIGNATURE;
}

/// Identifier for a request to be used for cancellation.
pub type RequestId = u32;

impl Type for crate::model::UsbState {
const SIGNATURE: &'static Signature = TAG_VALUE_SIGNATURE;
}
Expand Down Expand Up @@ -587,18 +713,27 @@ where
.map_err(|err| D::Error::custom(format!("could not deserialize from structure: {err}")))
}

#[derive(Serialize, Deserialize, Type)]
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
pub struct ViewRequest {
pub operation: Operation,

/// ID of the request.
pub id: RequestId,

/// The RP ID
pub rp_id: String,

/// Details about the application requesting credentials.
pub requesting_app: RequestingApplication,

/// Initial list of device interfaces that may provide credentials.
pub initial_devices: Vec<Device>,

/// Client window handle.
pub window_handle: Optional<WindowHandle>,
}

#[derive(Type, PartialEq, Debug)]
#[derive(Clone, Debug, PartialEq, Type)]
#[zvariant(signature = "s")]
pub enum WindowHandle {
Wayland(String),
Expand Down
1 change: 1 addition & 0 deletions credentialsd-ui/po/LINGUAS
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
en_US
de_DE
ka_GE
Loading
Loading