diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..c9bd9d72 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,268 @@ +This document describes the high-level architecture of `credentialsd`. If you +want to familiarize yourself with the code base, you are just in the right +place! + +# High-level Overview + +There are three APIs defined in [doc/api.md](/doc/api.md). This repository contains +two services that implement the three APIs defined by the specification: + +- `credentialsd`: Implements the Gateway API and Flow Control API +- `credentialsd-ui`: Implements the UI Control API. + +These two services communicate with each other over D-Bus IPC. + +The **Gateway** is the entrypoint for clients to interact with. The Flow +Controler and UI Controller work together to guide the user through the +process of selecting an appropriate credential based on the request received by +the Gateway. + +The **UI Controller** is used to launch a UI for the user to respond to +authenticator requests for user interaction. The **Flow Controller** interacts +with the OS and hardware, like detecting available transports and +authenticators. It then relays the information needed for the UI to guide the +user through the authentication flow, like prompts for a user to enter their PIN +or touch the device. The UI Controller takes user input and responds back to the +Flow Controller. + +The UI Controller and Flow Controller pass user interaction request and action +messages back and forth until the authenticator releases the credential. Then, +the Flow Controller sends the credential to the Gateway, which relays the +credential to the client. + +Here is a diagram of the intended usage and interactions between the APIs. + +```mermaid +sequenceDiagram + participant C as Client + participant G as Gateway + participant U as UI Controller + participant F as Flow Controller + participant A as Authenticator + + C ->> +G: Initiate request + G ->> U: Launch UI + U ->> F: Subscribe to events + loop + F ->> +A: Send control messages + A ->> F: Request user interaction + F ->> U: Request user interaction + U ->> F: Respond with user interaction + end + A ->> -F: Release credential + F ->> G: Respond with credential + G ->> -C: Respond with credential +``` + +The division into multiple services and APIs has two purposes: + +- least privilege: if there is a vulnerability in the UI process, it shouldn't + have access to interact with the credential service memory directly. Also, + hosting separate D-Bus services allows us to set different access control + policies on the bus, restricting communication on the Flow Control and UI + Control APIs to the `credentialsd` and `credentialsd-ui` services. + +- flexibility: UIs are very specific to their desktop environment, so it would + be impossible to satisfy the style and requirements for all the various Linux + desktop environments out there. This separation allows desktop environments to + create their own UIs targeted for their users. + +# Code Map + +## credentialsd + +A Rust binary project for the service hosting the Gateway and Flow Control APIs. +Interacts with authenticators and clients/user agents. + +`credentialsd` does not start the UI directly; it sends a request to start the +UI via D-Bus. It relies on D-Bus service activation or some other external +method to start the process that hosts the UI Control API. + +Credential requests on the Gateway are handled one at a time; if a new request +comes in, it is immediately rejected, and the client is expected to retry if +necessary. Currently, clients cannot cancel their own requests; the user has to +do that via the UI. + +### `credentialsd/src/credential_service/` + +`CredentialService` is the main component that interacts with authenticators. It +also holds request context to return back to the Gateway for request completion +or when the Flow Controller notifies it that the request is cancelled. + +Various authenticator transports are handled in sub-modules, for now USB and +hybrid transports are supported. Each handler starts a `Stream` of events that +represents requests from the authenticator for user interaction. If a response +is required from the user, the event should contain a channel for the credential +service to send the response after it receives user input. + +The credential service mostly just forwards events over to the UI service, minus +any details that are not necessary for the UI to know (like the response +channels mentioned above, which cannot be serialized over D-Bus anyway). + +Actual interaction I/O is performed in the [libwebauthn][libwebauthn] library. + +[libwebauthn]: https://github.com/linux-credentials/libwebauthn + +### `credentialsd/src/dbus/` + +D-Bus clients and services. + +The Gateway and Flow Controller services are defined here, as well as a client +for the UI Controller. + +The `model` module contains some methods to convert from D-Bus types to internal +credential service types. (These types don't need to be made known to the UI, so +they do not live in `credentialsd-common`). + +### `credentialsd/src/webauthn.rs` + +Types and functions needed to repackage requests from and responses to +JSON-strings according to the [WebAuthn spec](webauthn-3). + +There is one notable deviation from the spec: since we use JSON strings for +requests and responses, raw binary fields need to be base64url-encoded strings. +(See the note on [D-Bus/JSON serialization][dbus-json-serialization] in the API +docs.) It is the responsibility of the application using this service to +de-/construct the field accordingly. + +Re-exports many types from `libwebauthn`. + +[webauthn-3]: https://www.w3.org/TR/webauthn-3 +[dbus-json-serialization]: /doc/api.md#d-busjson-serialization + +### `credentialsd/tests` + +The `tests/` directory contains a setup for integration tests, allowing +`credentialsd` to connect to a test D-Bus instance. There is currently only a +few tests there; this should be expanded in the future. + +## `credentialsd-common/` + +Rust types shared between `credentialsd` and `credentials-ui`. + +Most of the types live in `src/model.rs`, and some are duplciated in +`src/server.rs`. The duplicates in the `server` module have tweaks that make it +easier to serialize over D-Bus, but more difficult to work with in Rust. So +conversion methods are provided between the two modules. + +## `credentialsd-ui/` + +A reference implementation for the UI Control API. + +This is a GTK4 implementation of a UI Controller. We don't intend this to fully +polish the UI, but it is provided as a reference for other desktop environment +developers to understand how to work with the API. + +### `credentialsd-ui/src/view_model/mod.rs` + +This contains an event loop that listens for events from the Flow Control API +and forwards them to the GTK UI. + +The view model is written in a way that is GUI framework-agnostic (except for +the fact that it uses async-std, and some frameworks may prefer Tokio or another +async runtime.) This is intended to aid other GUI developers using other +frameworks to set up their projects. For a given framework, it may be more +intuitive not to have this separation or to structure the code differently, but +the separation fo concerns here makes it clear what the developer needs to do. + +### `credentialsd-ui/src/view_model/gtk/` + +Contains GTK-specific code for drawing the UI. + +## `doc/` + +Contains spec-level documentation. + +Some of this is leftover from the first prototype. The documentation to pay +attention to is `api.md` which describes the D-Bus API and expected patterns. + +## `dbus/` + +Contains D-Bus service description files for D-Bus service activation, as well +as service policy files. + +During development, changes to these require pushing to the correct directory +`/usr/local/share/dbus-1/services` (or `system-services` if running on the +system bus), and a restart. + +## `systemd/` + +systemd service definition files for managing D-Bus service activation via systemd. + +During development, changes to these require pushing to the correct directory +`/usr/local/lib/systemd/user` or `system` if running as a system service and +reloading systemd's configuration with `systemctl daemon-reload`. + +## `webext/` + +A web extension that uses native messaging features to interact with the D-Bus +API via the browser Credentials Management/WebAuthn API. + +The `add-on/` directory contains the JavaScript and manifest that is loaded into +the browser, and the `app/` directory contains a Python script that proxies +messages between the browser and the D-Bus service. + +This is intended to be temporary; eventually we would like support for this API +to be upstreamed into Firefox and Chromium. + +## `demo_client/` + +A demo RP client that can be used for testing during development. + +This is a Python client that mimics an RP, saving the created public keys to a +local `user.json` file and verifying assertions against it. + +You can use it like this: + +```shell +cd demo_client/ +./main.py create +./main.py get +``` + +# Future Goals + +## LSM Hardening + +We should use all the LSM features possible, including SELinux, AppArmor and +Landlock. We should also try to use `seccomp` where those fall short. + +## Application Identity + +Currently, credentialsd assumes that clients have the ability to request any +origin. We should reduce this to only allowing some preconfigured list of +trusted or "privileged clients" to request any origin. + +What "trusted clients" means is not yet defined. This could be something like +LSM labels, or even just a list of paths. + +This list will need to be configurable by distros at least at install time, if +not compile time. Steps must be taken to prevent modification of that list at runtime. + +## Origin Binding + +Eventually, if the application identity problem above is solved, then we would +also like to allow certain clients access to request preconfigured origins based +on a correlation between the application identity and the origin. We call these +"unprivileged clients". + +Potentially, this may mean hard-coding a list of trusted proxies, like the +Flatpak or Snap daemons, to be trusted to set application identity properly. The +applications they proxy access for would then be considered unprivileged +clients. + +(Whether this preconfiguration means that clients not on the list cannot call +the D-Bus service at all, or whether they just have no binding to the origin is +probably an implementation detail, or defense in depth.) + +For more discussion on application identity and origin, see the +[Origin Checking discussion][origin-discussion] on GitHub. + +[origin-discussion]: https://github.com/linux-credentials/credentialsd/discussions/11 + +## Sandboxing + +Eventually, we would like to sandbox I/O, like USB and Bluetooth, into separate +processes so that malicious or buggy authenticators cannot corrupt the main +service's memory or access privileged files. For now, all authenticator I/O and +D-Bus service logic happens in the same process. diff --git a/README.md b/README.md index 1fdbfefe..66bbf637 100644 --- a/README.md +++ b/README.md @@ -57,18 +57,6 @@ with `credentialsd` directly without an add-on. You can access a [firefox-patch-flatpak]: https://download.opensuse.org/repositories/home:/MSirringhaus:/webauthn_devel/openSUSE_Factory_flatpak/ -## Clients - -There is a demo client in the `demo_client`. It mimics an RP, saving the created public keys to a local file and verifying assertions against it. - -```shell -cd demo_client/ -./main.py create -./main.py get -``` - -There is also a demo web extension that can be used to test the service in Firefox. Instructions are in [/webext/README.md](). - ## Mockups Here are some mockups of what this would look like for a user: diff --git a/demo_client/main.py b/demo_client/main.py index a5bf25ad..05f2cc09 100755 --- a/demo_client/main.py +++ b/demo_client/main.py @@ -18,7 +18,7 @@ async def run(cmd): bus = await MessageBus().connect() - with open("../contrib/xyz.iinuwa.credentialsd.Credentials.xml", "r") as f: + with open("../doc/xyz.iinuwa.credentialsd.Credentials.xml", "r") as f: introspection = f.read() proxy_object = bus.get_proxy_object( diff --git a/doc/api.md b/doc/api.md index 016ad00d..e1d049bb 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1,6 +1,7 @@ # API Overview There are three main API defined by this specification: + - [Gateway API](#gateway-api) - [Flow Control API](#flow-control-api) - [UI Control API](#ui-control-api) @@ -59,6 +60,78 @@ _privileged_ client: a client that is trusted to set any origin for its requests _relying party_: an entity wishing to auhtenticate a user _unprivileged client_: a client that is constrained to use a predetermined set of origin(s) +# General Notes + +## D-Bus/JSON serialization + +> TODO: rename fields to snake_case so that this note is true in all cases. + +This API is modelled after the [Credential Management API][credman-api]. The +top-level fields corresponding to `navigator.credentials.create()` and `get()` +are passed as fields in D-Bus dictionaries using snake_case, according to D-Bus +convention. + +So where Credential Management takes: + +```json +{ + "origin": "example.com", + "topOrigin": "example.com", + "password": true +} +``` + +this API takes: + +``` +[a{sv}] { + origin: Variant(""), + top_origin: Variant(""), // topOrigin is changed to top_origin + password: Variant(true), +} +``` + +However, for the complex requests and responses in the WebAuthn `create()` and `get()` +methods, this API passes JSON-encoded data as a string. Field and enum values +inside the JSON string should remain in camelCase. + +Additionally, `ArrayBuffer` objects, which are valid in JavaScript but cannot be +serialized in JSON, must be encoded as base64url strings with padding removed. + +So if a client passed this in JavaScript: + +```javascript +{ + "origin": "example.com", + "topOrigin": "example.com", + "publicKey": { + "challenge": new Uint8Array([97, 32, 99, 104, 97, 108, 108, 101, 110, 103, 101]), + "excludeCredentials": [ + {"type:" "public-key", "alg": -7} + ], + // ... + } +} +``` + +it would pass this request to this API: + +``` +[a{sv}] { + origin: Variant(''), + top_origin: Variant(''), // top-level fields topOrigin and publicKey are + public_key: Variant([a{sv}] { // changed to snake_case + registration_request_json: [s] '{ // <- JSON-encoded string + "challenge": "YSBjaGFsbGVuZ2U", // buffer is encoded as base64url without padding + "excludeCredentials": [ // "excludeCredentials" is not changed to snake_case + {"type": "public-key", "alg": -7} // "public-key" is not changed to snake_case + ] + // ... + }' + }) +} +``` + # Gateway API The Gateway is the entrypoint for public clients to retrieve and store @@ -95,6 +168,7 @@ CredentialType[s] [ ``` #### Request context + > TODO: replace is_same_origin with topOrigin, required if origin is set. > TODO: Should we say that `origin` will be optional in the future? @@ -117,6 +191,7 @@ suffix, as defined by the [Public Suffix List][PSL]. [PSL]: https://github.com/publicsuffix/list #### Credential Types + > TODO: decide on case of strings (snake_case like D-Bus or camelCase like JS?) Currently, there is only one supported type of `CreateCredentialRequest`, @@ -138,9 +213,10 @@ corresponds to WebAuthn credentials: type. ### Response + > TODO: Should we group common types in their own section for reference? -> CredentialType will be referenced in the request and response of both create -> and get methods. +> CredentialType will be referenced in the request and response of both create +> and get methods. `CreateCredentialResponse` is a polymorphic type that depends on the type of the request sent. Its `type` field is a string specifies what kind of @@ -208,6 +284,7 @@ When multiple credential types are specified, the request context applies to all credentials. #### Credential Types + > TODO: decide on case of strings (snake_case like D-Bus or camelCase like JS?) Currently, there is only one supported type of credential, specified by the @@ -226,9 +303,10 @@ GetPublicKeyCredentialOptions[a{sv}] { [def-pubkeycred-request-options]: https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions ### Response + > TODO: Should we group common types in their own section for reference? -> CredentialType will be referenced in the request and response of both create -> and get methods. +> CredentialType will be referenced in the request and response of both create +> and get methods. `GetCredentialResponse` is a polymorphic type that depends on the type of the request sent. Its `type` field is a string specifies what kind of credential it @@ -419,10 +497,11 @@ The device needs evidence of user presence (e.g. touch) to release the credentia `value`: No associated value. #### UsbState::SELECT_CREDENTIAL + > TODO: Change tense of verb to match other states -> SELECTING_CREDENTIAL > TODO: field names of Credential type are confusing: "name" is an ID, and -> "username" is a name. We should flip them. +> "username" is a name. We should flip them. Multiple credentials have been found and the user has to select which to use @@ -468,6 +547,7 @@ ServiceError[?] [ INTERNAL, ] ``` + #### ServiceError::AUTHENTICATOR_ERROR Some unknown error with the authenticator occurred. @@ -497,6 +577,7 @@ Something went wrong with the credential service itself, not the authenticator. `type`: `"INTERNAL"` ### HybridState + > TODO: Failed has no reason ``` @@ -588,7 +669,7 @@ Failed to receive a credential from the hybrid authenticator. > you would normally think as devices. Maybe "sources" works better? > TODO: CredentialMetadata is a bad name here, since this more corresponds to -the "devices" or "sources" concept. Change to DeviceMetadata? +> the "devices" or "sources" concept. Change to DeviceMetadata? This retrieves the various "devices" that the user can choose from to fulfill the request, filtered by the request origin and other request options. @@ -683,6 +764,7 @@ selects which credential to release based on the authenticator. ### Response None. + ### Errors TBD. @@ -754,6 +836,7 @@ authentication methods, and the user's device can offer a consistent user interface. So the Credentials API differs from the Secret Service API in two main ways: + - It supports specific credential formats (e.g. WebAuthn/FIDO2 credentials), rather than general secrets. - It is primarily focused on authenticating to relying parties. diff --git a/contrib/meson.build b/doc/meson.build similarity index 100% rename from contrib/meson.build rename to doc/meson.build diff --git a/doc/xyz.iinuwa.credentials.CredentialManager.xml b/doc/xyz.iinuwa.credentials.CredentialManager.xml deleted file mode 100644 index db9ee044..00000000 --- a/doc/xyz.iinuwa.credentials.CredentialManager.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/xyz.iinuwa.credentialsd.Credentials.xml b/doc/xyz.iinuwa.credentialsd.Credentials.xml similarity index 100% rename from contrib/xyz.iinuwa.credentialsd.Credentials.xml rename to doc/xyz.iinuwa.credentialsd.Credentials.xml diff --git a/meson.build b/meson.build index e61c74fa..e0f24a92 100644 --- a/meson.build +++ b/meson.build @@ -32,4 +32,4 @@ subdir('credentialsd-ui') subdir('dbus') subdir('systemd') subdir('webext') -subdir('contrib') \ No newline at end of file +subdir('doc') \ No newline at end of file diff --git a/webext/README.md b/webext/README.md index 2c0edb19..83be2fbd 100644 --- a/webext/README.md +++ b/webext/README.md @@ -55,7 +55,7 @@ couple of options: 1. Copy `webext/app/credential_manager_shim.json` to `~/.mozilla/native-messaging-hosts/credential_manager_shim.json`. 2. In `webext/app/credential_manager_shim.py`, point the `DBUS_DOC_FILE` variable to the absolute path to - `contrib/xyz.iinuwa.credentialsd.Credentials.xml`. + `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`