We're open-sourcing the library that powers 1Password's ability to log in with a passkey

We're open-sourcing the library that powers 1Password's ability to log in with a passkey

René Léveillé by René Léveillé on

You may have heard that 1Password beta testers can sign into websites using passkeys stored in their vaults. We’re actively developing the internal library powering passkey authentication, and now we’re open-sourcing it!

You can use the same passkey crate that powers 1Password’s authenticator to develop a WebAuthn client and/or authenticator. The passkey v0.1.0 crate is an easy access crate that doesn’t implement anything itself. Instead, it re-exports the other crates as modules:

We’re also open-sourcing our public-suffix library, which is based on the one from the Go standard library. Before setting off any language wars, please read the FAQ below for the reasons why.

All of these libraries are released at version 0.1 as they are still in relatively heavy development to support Android 14’s new credentials library and Apple’s updated Authentication Services APIs, which will release with iOS 17. We are also planning on adding more features and making the API easier to use, so we expect breaking changes to happen fairly frequently as we continue to develop and polish the passkey features.

To get a feel for how it works, you can try it out on any of the websites in passkeys.directory by starting a free 14-day trial. Or if you maintain an open-source project, you can apply for a free team account.

Now let me answer a few questions which I’m certain your fingers are itching to ask in the comments.

Why not use webauthn-rs?

Let me preface the following by saying that this library is very well-written and I highly recommend it if you are implementing a rust-powered website backend. It makes the setup so easy. It’s even what powered our passkey demo website for a while. When we started out building this feature we began with a fork, until we hit the following issues.

Typeshare

Not long ago, we open-sourced Typeshare. We use this utility throughout our codebase to communicate between our Rust core and all our front-ends. Naturally we want to use this to pass the requests from the browser extension’s TypeScript to the Rust core compiled to WASM.

For the code-gen to work, the annotation needs to be defined on the type definition and that definition needs to be in-tree. So a hard fork is necessary to set typeshare on all the types without having to redefine these in TypeScript. We also can’t use the ones defined in the Web APIs as they’re defined with ArrayBuffers for all the array types. These can’t simply be serialized with JSON.stringify, so we have to convert the types to something that can be serialized for Typeshare to work.

WASM compatibility

We use Rust at the core of all our client applications, including the browser extension through WASM. Unfortunately, webauthn-rs uses OpenSSL as their cryptographic library, and the Rust wrapper doesn’t officially support WASM. Not only that, but we already bundle ring and other RustCrypto libraries, so why bundle a third library – especially when WASM has a set limit of methods it can have in a bundle? We try our best to keep that number as low as possible.

Forking the library and replacing the cryptography library isn’t hard though, and that’s what we initially did! Until we hit the next issue.

CTAP2 support

There are a multitude of FIDO-defined specifications for authenticators. There’s U2F, UAF and CTAP. We’re interested in the first and last. Universal 2nd Factor (U2F) is one of the first specifications for WebAuthn authenticators, and it’s easy to implement. The issue is that development has concluded in favor of the Client To Authenticator Protocol (CTAP2), where the 2 in CTAP2 indicates the version, which can be thought of as a successor to U2F.

So what does this have to do with webauthn-rs? At the time, in summer 2022, webauthn-authenticator-rs only really implemented U2F. The issue with this is that passkeys are a credential type that was developed to only support CTAP2. We would have to write an entirely new implementation. We could have upstreamed this new implementation to webauthn-rs, but we would still be stuck with a fork to replace the cryptography. So we developed our own library.

This turned out to be a good choice, since now it could fit nicely with the architecture of our existing codebase instead of being full of workarounds. This also allowed us to grow the library organically to fit our changing needs instead of worrying about keeping the fork compatible with an upstream version. This is a big reason why, when we learned that CTAP2 was being worked on in webauthn-rs, we opted to stick with our implementation instead.

Why not use psl?

The public suffix list (psl) library is indeed used by a lot of people and existing libraries. It even has a variant that allows you to dynamically load a new list if you have custom changes, which we do for our browser auto-filling features. The difference comes down to the codegen.

WASM compatibility

Since the psl crate is pure Rust, it compiles to WASM seamlessly… until you compile it as part of a large application bundle. As soon as we added the crate to our dependency tree, we broke WASM compilation by hitting the method number limit we mentioned regarding bundling OpenSSL. The issue stems from the fact that the codegen for the psl crate creates a method for each item in that list. They’re marked with the #[inline] directive, but our best guess is that the compiler decided to ignore those.

Performance

Our very un-scientific bench test showed significant latency in our suggestions for autofill when using psl in comparison to our previous public-suffix implementation, which has been driving our domain extraction since the inception of 1Password 8. As mentioned earlier, this is based on the Go version which uses a highly optimized lookup table. This gives us the performance we need to give suggestions at an acceptable speed, all while only adding six methods to our WASM bundle.

For these reasons, we decided it would be less work to open-source our implementation and use that as a dependency in passkey-client rather than trying to make psl work for our context.

Get started with passkey-rs

If you want to get a feel for how passkey works, check out our example, or try using it yourself by adding passkey = “0.1” to your project’s Cargo.toml. Or if you only want to use a single part, check out any of the sublibraries we’re announcing today:

If you just want to read code, browse the source code on GitHub where you can also see our other open-source libraries like Typeshare. If you find anything or simply want to collaborate, reach out by creating an issue or a pull request on the repository.

Developer

René Léveillé - Developer René Léveillé - Developer

Tweet about this post