The web is a wonderful place: open, permissionless, linkable and composable, but also a dangerous place with so many actors: vulnerable and malicious.
Self-custody key is difficult, and Passkey is, in effect, going to be a custodial solution, so you need a separate app outside of the web, but this is friction.
The ideal is to structure the isolated and secure environment and bridge only the results of signing/decryption to general web apps/extensions, with fluentness. Here is one of paths to it.
For example, it enables users to:
Overview
The browser turns itself into the foundation to help your web apps/extensions make self-custody, providing new dedicated credential store and APIs. Therefore, you can easily build self-custody apps using two APIs, window.ssi
and browser.ssi
.
Browser will not provide so-called “wallet” because store and setting don’t/shouldn’t have the ability to communicate to external.
SSI Store Service
It is the internal dedicated store for credentials such like secret key and authorization data, which is protected with isolated process model and encryption. Furthermore, it is the actual entity that executes the tasks related to credentials such like sign/decrypt. Separation of concerns provides a consistent service interface regardless of where the actual data resides (e.g., on the file system, on a Secure Element (SE), or on separate hardware). Accessed via internal privileged services in browser.ssi
, window.ssi
, and browser setting.
browser.ssi
It is the core API (if chrome, it would be named chrome.ssi
), which bridges the tasks related to credentials such like sign/decrypt between internal module and user land. It also provides setting states while paying attention to privacy and security, basically which are what user can choose whether to make open. General web extensions can use it in their scripts.
window.ssi
It is the most accessible API that is widely published on the web, which bridges the tasks related to credentials such like sign/decrypt between internal module and user land. General web extensions and general web apps can use it anywhere.
Browser setting
It is the user interface to the SSI Store Service, providing key generation and API configuration in the privileged process that differs from general web apps.
Developer Experience
In this section we’ll explain how web app development changes when browsers have self-custodial key management built in and browser features designed to integrate self-custodial key directly.
Benefits for web developers
We can’t imagine exactly what that would cause, but can at least say the following:
- The keys are isolated, concealed, and not accessible to any general web app.
- You, web developers, no longer need key management by delegating it to the browser
- You would just use native APIs exposed for web apps
It’s worth noting that this is self-custody, not custody, because custody is similar experience to the web developers, as you only access the API and don’t need key management. On the other hand, there is a deep gap for the users between the two, as any Bitcoiner knows, “Not your keys, not your coins”.
However, self-custody is very hard. We won’t go into many of it here - you’ll have to read other texts to find out - but the point is that some of the difficulties of self-custody can be solved by having the browser receive the delegation. It’s important to also note that we never intended for the browser to have any stored assets - just spending online.
That is similar to being put in the web storage and being kept by extension, but any web app can’t access the key. That is similar to treating API via external remote service that holds the key (while being non-custodial), but it can be there from the moment the tab creates and is persisted forever. You may not worry anymore about leakage risks and race conditions.
Browser architecture and APIs
To explain about how this works, let’s modify the diagram from the previous section to focus on the app side, and notice the distinction between “cached data” and “secret key”.
“cached data” is put in Local storage, Indexed DB and so on, “secret key” is in the internal store. Both are accessed by the app through a global window. However, secret key is, in addition to the usual web sandbox model, further protected by the browser’s process security model.
Taking Firefox as an example, the “secret key” is in the Parent Process and the “cached data” is anywhere, typically WebContent Process, in the Child Process.
cite: https://firefox-source-docs.mozilla.org/dom/ipc/process_model.html
These Inter-Process Communication (IPC) operations require privileged permissions and security policy, and the bridge to user land is handled by the native APIs, browser.ssi and window.ssi.
Taking Firefox again, the “chrome:” on the right corresponds to those native APIs and has privileged permissions over the parent process, while interference from the left in user land is banned.
cite: https://firefox-source-docs.mozilla.org/dom/scriptSecurity/index.html
It means that user keys are not affected even if your web app itself unfortunately - no matter how carefully and conscientiously you develop - becomes vulnerable or malicious enough to steal them. Those problems may be the same about browser software, but it is less susceptible to supply chain attacks due to its limited leverage of package ecosystems (much of it is hand-coded) and more robust due to being centered around the process model described above and significantly limited external communications; the attack surface is much smaller.
Although not as much as completely separated hardware or paper would, it hopefully makes the secret key truly secret and minimizes the risk of leakage by web apps. And, importantly, online usage will be more fluent than hardware or paper, and on par with general web apps that hold keys.
Developing App
Currently this is individual project and you can experience those APIs and develop the app in a custom browser of our reference implementation based on Firefox ESR.
This section provides a guide to using that browser as an example and developing apps that run on top of it.
Please note that fork browsers not part of the official distribution may be restrictive implementations in various respects: It is an experimental version.
Installation
The custom browser add-ons four main components for Self-Sovereign Identity - window.ssi
, browser.ssi
, services.ssi
(ssi store service) and about:selfsovereignidentity
(setting page) - in the Firefox ESR.
Also, those components can easily add-on to other firefox forks as well, so you can choose another implementation. And your implementation, too!
Please choose the best for you.
System Requirements
https://www.mozilla.org/en-US/firefox/128.5.0/system-requirements/
Install from source
- Set up your editor - https://firefox-source-docs.mozilla.org/contributing/editor.html
- Check out from https://gitlab.com/studioteatwo/gecko-dev-for-ssi
git clone git@gitlab.com:studioteatwo/gecko-dev-for-ssi.git --depth 1
- Build
cd gecko-dev-for-ssi
./mach build
- After that, you can choose
./mach run
(for interactive) or./mach package
(for static).
Install from the binaries
Download the one for your platform from the distribution site.
Currently these builds are pre-release-style release. Please hold the ctl key and right-click on Mac and Windows.
https://github.com/studioTeaTwo/gecko-dev-pkg-distributor/releases
Install other firefox forks
Tor Browser version
- Set up your editor - https://firefox-source-docs.mozilla.org/contributing/editor.html
- Check out from https://gitlab.com/studioteatwo/gecko-dev-for-ssi
git clone git@gitlab.com:studioteatwo/gecko-dev-for-ssi.git --depth 1
git checkout mvp-tor
Configure
Depending on your build, you may need to configure manually in about:config
.
requirement | key | value |
---|---|---|
MUST | security.nocertdb | false |
Versioning
These builds are forked and therefore managed by versioning both the custom add-on browser and the base browser. The version is difined below:
v<custom-browser-version><custom-browser-branch>-<base-browser-version><base-browser-suffix>
For example v0.0.1mvp-128.5.0esr
is destined for a 0.0.1 release from mvp branch, based on the Mozilla Firefox 128.5.0 ESR release. If v0.0.1beta-14.0-1tor
, it means a 0.0.1 release from beta branch, based on the Tor browser 14.0-1 release.
And, to distinguish with the branding, the install path and so on of the base browser, the namespace ssb
is defined.
Using window.ssi
window.ssi
is accessible anywhere that window
is visible. This means that generally it can be accessed by web apps loaded in tab, and in-page scripts (in some cases content scripts as well) in web extensions, etc.
It mainly serves the access to user’s public key and the mediation to the tasks by user’s secret key.
There are various ways to implement window.ssi, such as native window system, web extension, etc. Depending on how it’s implemented, it may also be injected into which frame. In any case it is always present in at least the top frame.
If implemented by a web extension (currently so), window.ssi is inserted around the DOMContentLoaded event and it is the best that you access after the load event.
Basic usage
You call it in the same way as window.location
, window.navigator
, etc.
const publicKey = await window.ssi.nostr.getPublicKey()
const user = someNostrService.getUser(publicKey)
Listening to event
You listen the event as CustomEvent.
const accountChangedHandler = (event: CustomEvent<string>) => {
const newPublicKey = event.detail
doSomething(newPublicKey)
}
window.ssi.nostr.addEventListener("accountChanged", accountChangedHandler)
Wrapping in protocol standard
In most cases, you would wrap just the parts about the key management in a protocol standard like NIP-07.
const Nip07 = {
async getPublicKey(): Promise<PulicKey> {
return window.ssi.nostr.getPublicKey()
},
async signEvent(event: {
kind: number
content: string
tags: string[][]
created_at: number
}): Promise<NostrEvent> {
event.pubkey = await this.getPublicKey()
const eventHash = serializeEvent(event)
const sig = await window.ssi.nostr.sign(eventHash, { type: "signEvent", event })
return {...event, id: eventHash, sig}
}
}
window.nostr = Nip07
Using browser.ssi
browser.ssi
is the underlying API that is more powerful than window.ssi. It does everything window.ssi does, and more:
- Can read all the credentials across protocols (if the user self-sovereignly does allow)
- Can read the user’s setting states (as long as there are no privacy and security issues)
- Can integrate element APIs to create any flow you want
Instead, this API is limited where you can use. It would be assumed to be built by Built-in API or Experimental API of Firefox based on WebExtensions, so you can access through a web extension.
Basically, this API can be called in background script, but possibly content script and devtools as well. You can check what scope an browser.ssi has in your using implementation by looking at ssi’s manifest file. See for example here.
Set up
To use this API, you declare in the permissions
property in your WebExtension’s manifest.json
file. For example:
{
"manifest_version": 2,
"name": "Example Web Extension",
"permissions": ["ssi"],
}
If you want the subset of the specific protocol, please add the one with ssi
together.
{
"permissions": ["ssi", "ssi.nostr"],
}
Basic usage
The ssi declared in the permissions property in your manifest file is injected into the global object browser
.
await browser.ssi.searchCredentialsWithoutSecret({
protocolName,
credentialName,
})
Search store
Example of a full search for SSI store service:
const credentials = await browser.ssi.searchCredentialsWithoutSecret({
primary: false
})
store.set(credentials.map(credential => doSomething(credential)))
In fact, the results returned are filtered by the user’s preferences, privacy and security reasons.
Request a task related to the key
You can sign and encrypt/decrypt using the internal key currently set as primary for a specific protocol.
const signature = await browser.ssi.nostr.sign(message)
You should always read the public key without using cache just before sign/encrypt/decrypt, as users may change their primary key without notifying you.
Get user settings
You will get what you need:
const preferences = await browser.ssi.nostr.getPrefs()
store.set(preferences)
Receive notifications when user settings status changes
You can listen to this as a special event.
const onPrimaryChangedCallback = async () => {
// Get the new primary key. If without authorization, auth dialog will be prompted.
const credentials = await browser.ssi.searchCredentialsWithoutSecret({
protocolName: "nostr",
credentialName: "nsec",
primary: true
})
const publicKey = decodeNpub(credentials[0].identifier)
// Send the message to the contents
const tabs = await browser.tabs.query()
for (const tab of tabs) {
browser.tabs
.sendMessage(tab.id, {
action: "accountChanged",
args: { publicKey },
})
}
}
browser.ssi.nostr.onPrimaryChanged.addListener(onPrimaryChangedCallback)
At first glance it looks similar to a normal EventListener, but it may be useful to know that it is a different mechanism to cross IPC.
Authorization
It’s always the libra between the UX and security for powerful features. Just like the history of camera and notification permissions, this one requires careful considerations and is one of the topics we explore the most: Authorization flow.
Currently, the combination of the trusted site method and password authentication is implemented in browser.ssi.askPermission
. This is a two-step method.
The trusted site that the user has explicitly declared trust for by specifying the app’s URL is the top level. If that is NG, the browser’s primary password or OS account password authentication is presented to the user. However, this is the self-sovereign browser, so, if both settings are explicitly disabled, it is accepted the user has self-sovereignly chosen that the app does not need the user’s permission to use their key, and the request will be allowed through unconditionally.
This can be done below.
// Authorization will be performed using the secret currently set as primary
// within the specified protocol name and credential name.
const permitted = await browser.ssi.askPermission(
"nostr", // protocol name
"nsec", // credential name
{
// A text description displayed on Auth dialog
caption: "sign a message",
// Reference data for considering auth
submission: '{"kind":1,"tags":[],"content":"","created_at":1737015477603,"pubkey":"af296d0bb54f5ce05baa7f5fdd01e5cb8fdd9789d637fadb7ce167729ba62c2c","id":"0d6e9316d020bbd19543dae112a8413a6543306124f1028f52e1aba6a4e59a40","sig":"3cc65050a3033e1d4458c6c3bc344f256c0fa4778d6f0f0049f1836e8b862fa3f1e22e26d7f9941132047b57675e32c828d7cf570936eb8963845680fe9b82d1"}',
}
)
window.ssi
Examples
window.ssi.nostr.getPublicKey()
Nostr
Namespace
ssi.nostr
APIs
type | name |
---|---|
function | getPublicKey |
function | sign |
event | accountChanged |
event | providerChanged |
browser.ssi
Examples
browser.ssi.searchCredentialsWithoutSecret({
protocolName,
credentialName,
})
General
Namespace
ssi
Required Permissions
"ssi"
APIs
type | name |
---|---|
function | searchCredentialsWithoutSecret |
function | askPermission |
Nostr
Namespace
ssi.nostr
Required Permissions
"ssi", "ssi.nostr"
APIs
type | name |
---|---|
function | getPrefs |
function | sign |
event | onPrimaryChanged |
event | onPrefEnabledChanged |