Expand description
webauthn_rp
is a library for server-side
Web Authentication (WebAuthn) Relying Party
(RP) operations.
The purpose of a server-side RP library is to be modular so that any client can be used with it as a backend
including native applications—WebAuthn technically only covers web applications; however it’s relatively easy
to adapt to native applications as well. It achieves this by not assuming how data is sent to/from the client;
having said that, there are pre-defined serialization formats for “common” deployments which can be used when
serde
is enabled.
§webauthn_rp
in action
use webauthn_rp::{
AuthenticatedCredential, DiscoverableAuthentication64, DiscoverableAuthenticationServerState,
DiscoverableCredentialRequestOptions, PublicKeyCredentialCreationOptions, RegisteredCredential,
Registration, RegistrationServerState,
request::{
AsciiDomain, PublicKeyCredentialDescriptor, RpId,
auth::AuthenticationVerificationOptions,
register::{
Nickname, PublicKeyCredentialUserEntity, RegistrationVerificationOptions,
USER_HANDLE_MAX_LEN, UserHandle64, Username,
},
},
response::{
CredentialId,
auth::error::AuthCeremonyErr,
register::{CompressedPubKey, DynamicState, error::RegCeremonyErr},
},
};
// These are available iff `serializable_server_state` is _not_ enabled.
use webauthn_rp::request::{FixedCapHashSet, InsertResult};
use serde::de::{Deserialize, Deserializer};
use serde_json::Error as JsonErr;
/// The RP ID our application uses.
const RP_ID: &str = "example.com";
/// Error we return in our application when a function fails.
enum AppErr {
/// WebAuthn registration ceremony failed.
RegCeremony(RegCeremonyErr),
/// WebAuthn authentication ceremony failed.
AuthCeremony(AuthCeremonyErr),
/// Unable to insert a WebAuthn ceremony.
WebAuthnCeremonyCreation,
/// WebAuthn ceremony does not exist; thus the ceremony could not be completed.
MissingWebAuthnCeremony,
/// General error related to JSON deserialization.
Json(JsonErr),
/// No account exists associated with a particular `UserHandle64`.
NoAccount,
/// No credential exists associated with a particular `CredentialId`.
NoCredential,
/// `CredentialId` exists but the associated `UserHandle64` does not match.
CredentialUserIdMismatch,
}
impl From<JsonErr> for AppErr {
fn from(value: JsonErr) -> Self {
Self::Json(value)
}
}
impl From<RegCeremonyErr> for AppErr {
fn from(value: RegCeremonyErr) -> Self {
Self::RegCeremony(value)
}
}
impl From<AuthCeremonyErr> for AppErr {
fn from(value: AuthCeremonyErr) -> Self {
Self::AuthCeremony(value)
}
}
/// First-time account creation.
///
/// This gets sent from the user after an account is created on their side. The registration ceremony
/// still has to be successfully completed for the account to be created server side. In the event of an error,
/// the user should delete the created passkey since it won't be usable.
struct AccountReg<'a, 'b> {
registration: Registration,
user_name: Username<'a>,
user_display_name: Nickname<'b>,
}
impl<'de: 'a + 'b, 'a, 'b> Deserialize<'de> for AccountReg<'a, 'b> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// ⋮
}
}
/// Starts account creation.
///
/// This only makes sense for greenfield deployments since account information (e.g., user name) would likely
/// already exist otherwise. This is similar to credential creation except a random `UserHandle64` is generated and
/// will be used for subsequent credential registrations.
fn start_account_creation(
reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
) -> Result<Vec<u8>, AppErr> {
let rp_id = RpId::Domain(
AsciiDomain::try_from(RP_ID.to_owned())
.unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
);
let user_id = UserHandle64::new();
let (server, client) =
PublicKeyCredentialCreationOptions::first_passkey_with_blank_user_info(
&rp_id, &user_id,
)
.start_ceremony()
.unwrap_or_else(|_e| {
unreachable!("we don't manually mutate the options; thus this can't fail")
});
if matches!(
reg_ceremonies.insert_or_replace_all_expired(server),
InsertResult::Success
) {
Ok(serde_json::to_vec(&client)
.unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")))
} else {
Err(AppErr::WebAuthnCeremonyCreation)
}
}
/// Finishes account creation.
///
/// Pending a successful registration ceremony, a new account associated with the randomly generated
/// `UserHandle64` will be created with a corresponding passkey entry. This passkey will be used to
/// log into the application.
///
/// Note if this errors, then the user should be notified to delete the passkey created on their
/// authenticator.
fn finish_account_creation(
reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
client_data: Vec<u8>,
) -> Result<(), AppErr> {
let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data.as_slice())?;
insert_account(
&account,
reg_ceremonies
// `Registration::challenge_relaxed` is available iff `serde_relaxed` is enabled.
.take(&account.registration.challenge_relaxed()?)
.ok_or(AppErr::MissingWebAuthnCeremony)?
.verify(
&RpId::Domain(
AsciiDomain::try_from(RP_ID.to_owned())
.unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
),
&account.registration,
&RegistrationVerificationOptions::<&str, &str>::default(),
)?,
)
}
/// Starts passkey registration.
///
/// This is used for _existing_ accounts where the user is already logged in and wants to register another
/// passkey. This is similar to account creation except we already have the user entity info and we need to
/// fetch the registered `PublicKeyCredentialDescriptor`s to avoid accidentally overwriting a passkey on
/// the authenticator.
fn start_cred_registration(
user_id: &UserHandle64,
reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
) -> Result<Vec<u8>, AppErr> {
let rp_id = RpId::Domain(
AsciiDomain::try_from(RP_ID.to_owned())
.unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
);
let (entity, creds) = select_user_info(user_id)?.ok_or_else(|| AppErr::NoAccount)?;
let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, entity, creds)
.start_ceremony()
.unwrap_or_else(|_e| {
unreachable!("we don't manually mutate the options; thus this won't error")
});
if matches!(
reg_ceremonies.insert_or_replace_all_expired(server),
InsertResult::Success
) {
Ok(serde_json::to_vec(&client)
.unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")))
} else {
Err(AppErr::WebAuthnCeremonyCreation)
}
}
/// Finishes passkey registration.
///
/// Pending a successful registration ceremony, a new credential associated with the `UserHandle64`
/// will be created. This passkey can then be used to log into the application just like any other registered
/// passkey.
///
/// Note if this errors, then the user should be notified to delete the passkey created on their
/// authenticator.
fn finish_cred_registration(
reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
client_data: Vec<u8>,
) -> Result<(), AppErr> {
// `Registration::from_json_custom` is available iff `serde_relaxed` is enabled.
let registration = Registration::from_json_custom(client_data.as_slice())?;
insert_credential(
reg_ceremonies
// `Registration::challenge_relaxed` is available iff `serde_relaxed` is enabled.
.take(®istration.challenge_relaxed()?)
.ok_or(AppErr::MissingWebAuthnCeremony)?
.verify(
&RpId::Domain(
AsciiDomain::try_from(RP_ID.to_owned())
.unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
),
®istration,
&RegistrationVerificationOptions::<&str, &str>::default(),
)?,
)
}
/// Starts the passkey authentication ceremony.
fn start_auth(
auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
) -> Result<Vec<u8>, AppErr> {
let rp_id = RpId::Domain(
AsciiDomain::try_from(RP_ID.to_owned())
.unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
);
let (server, client) = DiscoverableCredentialRequestOptions::passkey(&rp_id)
.start_ceremony()
.unwrap_or_else(|_e| {
unreachable!("we don't manually mutate the options; thus this won't error")
});
if matches!(
auth_ceremonies.insert_or_replace_all_expired(server),
InsertResult::Success
) {
Ok(serde_json::to_vec(&client).unwrap_or_else(|_e| {
unreachable!("bug in DiscoverableAuthenticationClientState::serialize")
}))
} else {
Err(AppErr::WebAuthnCeremonyCreation)
}
}
/// Finishes the passkey authentication ceremony.
fn finish_auth(
auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
client_data: Vec<u8>,
) -> Result<(), AppErr> {
// `Authentication::from_json_custom` is available iff `serde_relaxed` is enabled.
let authentication =
DiscoverableAuthentication64::from_json_custom(client_data.as_slice())?;
let mut cred = select_credential(
authentication.raw_id(),
authentication.response().user_handle(),
)?
.ok_or_else(|| AppErr::NoCredential)?;
if auth_ceremonies
// `Authentication::challenge_relaxed` is available iff `serde_relaxed` is enabled.
.take(&authentication.challenge_relaxed()?)
.ok_or(AppErr::MissingWebAuthnCeremony)?
.verify(
&RpId::Domain(
AsciiDomain::try_from(RP_ID.to_owned())
.unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
),
&authentication,
&mut cred,
&AuthenticationVerificationOptions::<&str, &str>::default(),
)?
{
update_credential(cred.id(), cred.dynamic_state())
} else {
Ok(())
}
}
/// Writes `account` and `cred` to storage.
///
/// # Errors
///
/// Errors iff writing `account` or `cred` errors, there already exists a credential using the same
/// `CredentialId`, or there already exists an account using the same `UserHandle64`.
fn insert_account(
account: &AccountReg<'_, '_>,
cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>,
) -> Result<(), AppErr> {
// ⋮
}
/// Fetches the user info and registered credentials associated with `user_id`.
///
/// # Errors
///
/// Errors iff fetching the data errors.
fn select_user_info<'a>(
user_id: &'a UserHandle64,
) -> Result<
Option<(
PublicKeyCredentialUserEntity<'static, 'static, 'a, USER_HANDLE_MAX_LEN>,
Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
)>,
AppErr,
> {
// ⋮
}
/// Writes `cred` to storage.
///
/// # Errors
///
/// Errors iff writing `cred` errors or there already exists a credential using the same `CredentialId`.
fn insert_credential(
cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>,
) -> Result<(), AppErr> {
// ⋮
}
/// Fetches the `AuthenticatedCredential` associated with `cred_id` ensuring `user_id` matches the
/// `UserHandle64` associated with the account.
///
/// # Errors
///
/// Errors iff fetching the data errors or the `user_id` does not match the stored `UserHandle64`.
fn select_credential<'cred, 'user>(
cred_id: CredentialId<&'cred [u8]>,
user_id: &'user UserHandle64,
) -> Result<
Option<
AuthenticatedCredential<
'cred,
'user,
USER_HANDLE_MAX_LEN,
CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>,
>,
>,
AppErr,
> {
// ⋮
}
/// Overwrites the current `DynamicState` associated with `cred_id` with `dynamic_state`.
///
/// # Errors
///
/// Errors iff writing errors or `cred_id` does not exist.
fn update_credential(
cred_id: CredentialId<&[u8]>,
dynamic_state: DynamicState,
) -> Result<(), AppErr> {
// ⋮
}
§Cargo “features”
custom
or both bin
and serde
must be enabled; otherwise a compile_error
will occur.
§bin
Enables binary (de)serialization via Encode
and Decode
. Since registered credentials will almost always
have to be saved to persistent storage, some form of (de)serialization is necessary. In the event bin
is
unsuitable or only partially suitable (e.g., human-readable output is desired), one will need to enable
custom
to allow construction of certain types (e.g., AuthenticatedCredential
).
If possible and desired, one may wish to save the data “directly” to avoid any potential temporary allocations.
For example StaticState::encode
will return a Vec
containing hundreds (and possibly thousands in the
extreme case) of bytes if the underlying public key is an RSA key. This additional allocation and copy of data
is obviously avoided if StaticState
is stored as a
composite type or its fields are stored in separate
columns when written to a relational database (RDB).
§custom
Exposes functions (e.g., AuthenticatedCredential::new
) that allows one to construct instances of types that
cannot be constructed when bin
or serde
is not enabled.
§serde
This feature strictly adheres to the JSON-motivated definitions. You will encounter clients that send data
that cannot be deserialized using this feature. For many serde_relaxed
should be used
instead.
Enables (de)serialization of data sent to/from the client via serde
based on the JSON-motivated definitions (e.g.,
RegistrationResponseJSON
). Since
data has to be sent to/from the client, some form of (de)serialization is necessary. In the event serde
is unsuitable or only partially suitable, one will need to enable custom
to allow construction
of certain types (e.g., Registration
).
Code is strongly encouraged to rely on the Deserialize
implementations as much as possible to reduce the
chances of improperly deserializing the client data.
Note that clients are free to send data in whatever form works best, so there is no requirement the JSON-motivated definitions are used even when JSON is sent. This is especially relevant since the JSON-motivated definitions were only added in WebAuthn Level 3; thus many deployments only partially conform. Some specific deviations that may require partial customization of deserialization are the following:
ArrayBuffer
s encoded using something other than base64url.ArrayBuffer
s that are encoded multiple times (including the use of different encodings each time).- Missing fields (e.g.,
transports
). - Different field names (e.g.,
extensions
instead ofclientExtensionResults
).
§serde_relaxed
Automatically enables serde
in addition to “relaxed” Deserialize
implementations
(e.g., RegistrationRelaxed
). Roughly “relaxed” translates to unknown fields being ignored and only
the fields necessary for construction of the type are required. Case still matters, duplicate fields are still
forbidden, and interrelated data validation is still performed when applicable. This can be useful when one
wants to accommodate non-conforming clients or clients that implement older versions of the spec.
§serializable_server_state
Automatically enables bin
in addition to Encode
and Decode
implementations for
RegistrationServerState
, DiscoverableAuthenticationServerState
, and
NonDiscoverableAuthenticationServerState
. Less accurate SystemTime
is used instead of Instant
for
timeout enforcement. This should be enabled if you don’t desire to use in-memory collections to store the instances
of those types.
Note even when written to persistent storage, an application should still periodically remove expired ceremonies.
If one is using a relational database (RDB); then one can achieve this by storing TimedCeremony::sent_challenge
,
the Vec
returned from Encode::encode
, and TimedCeremony::expiration
and periodically remove all rows
whose expiration exceeds the current date and time.
§Registration and authentication
Both registration and authentication ceremonies rely on “challenges”, and these challenges are inherently temporary. For this reason the data associated with challenge completion can often be stored in memory without concern for out-of-memory (OOM) conditions. There are several benefits to storing such data in memory:
- No data manipulation
- By leveraging move semantics, the data sent to the client cannot be mutated once the ceremony begins.
- Improved timeout enforcement
- By ensuring the same machine that started the ceremony is also used to finish the ceremony, deviation of
system clocks is not a concern. Additionally, allowing serialization requires the use of some form of
cross-platform “timestamp” (e.g., Unix time) which differ in
implementation (e.g., platforms implement leap seconds in different ways) and are often not monotonically
increasing. If data resides in memory, a monotonic
Instant
can be used instead.
- By ensuring the same machine that started the ceremony is also used to finish the ceremony, deviation of
system clocks is not a concern. Additionally, allowing serialization requires the use of some form of
cross-platform “timestamp” (e.g., Unix time) which differ in
implementation (e.g., platforms implement leap seconds in different ways) and are often not monotonically
increasing. If data resides in memory, a monotonic
It is for those reasons data like RegistrationServerState
are not serializable by default and require the
use of in-memory collections (e.g., FixedCapHashSet
). To better ensure OOM is not a concern, RPs should set
reasonable timeouts. Since ceremonies can only be completed by moving data (e.g.,
RegistrationServerState::verify
), ceremony completion is guaranteed to free up the memory used—
RegistrationServerState
instances are as small as 48 bytes on x86_64-unknown-linux-gnu
platforms. To avoid
issues related to incomplete ceremonies, RPs can periodically iterate the collection for expired ceremonies and
remove such data. Other techniques can be employed as well to mitigate OOM, but they are application specific
and out-of-scope. If this is undesirable, one can enable serializable_server_state
so that RegistrationServerState
, DiscoverableAuthenticationServerState
, and
NonDiscoverableAuthenticationServerState
implement Encode
and Decode
. Another reason one may need to
store this information persistently is for load-balancing purposes where the server that started the ceremony is
not guaranteed to be the server that finishes the ceremony.
§Supported signature algorithms
The only supported signature algorithms are the following:
- Ed25519 as defined in RFC 8032 § 5.1. This corresponds
to
CoseAlgorithmIdentifier::Eddsa
. - ECDSA as defined in SEC 1 Version 2.0 § 4.1 using SHA-256
as the hash function and NIST P-256 as defined in
NIST SP 800-186 § 3.2.1.3
for the underlying elliptic curve. This corresponds to
CoseAlgorithmIdentifier::Es256
. - ECDSA as defined in SEC 1 Version 2.0 § 4.1 using SHA-384 as the hash function and NIST P-384 as defined in
NIST SP 800-186 § 3.2.1.4
for the underlying elliptic curve. This corresponds to
CoseAlgorithmIdentifier::Es384
. - RSASSA-PKCS1-v1_5 as defined in RFC 8017 § 8.2 using
SHA-256 as the hash function. This corresponds to
CoseAlgorithmIdentifier::Rs256
.
§Correctness of code
This library more strictly adheres to the spec than many other similar libraries including but not limited to the following ways:
- CTAP2 canonical CBOR encoding form.
Deserialize
implementations requiring exact conformance (e.g., not allowing unknown data).- More thorough interrelated data validation (e.g., all places a Credential ID exists must match).
- Implement a lot of recommended (i.e., SHOULD) criteria (e.g., User display names conforming to the Nickname Profile as defined in RFC 8266).
Unfortunately like almost all software, this library has not been formally verified; however great care is employed in the following ways:
- Leverage move semantics to prevent mutation of data once in a static state.
- Ensure a great many invariants via types.
- Reduce code duplication.
- Reduce variable mutation allowing for simpler algebraic reasoning.
panic
-free code1 (i.e., define true/total functions).- Ensure arithmetic “side effects” don’t occur (e.g., overflow).
- Aggressive use of compiler and Clippy lints.
- Unit tests for common cases, edge cases, and error cases.
§Cryptographic libraries
This library does not rely on any sensitive data (e.g., private keys) as only signature verification is
ever performed. This means that the only thing that matters with the libraries used is their algorithmic
correctness and not other normally essential aspects like susceptibility to side-channel attacks. While I
personally believe the libraries that are used are at least as “secure” as alternatives even when dealing with
sensitive data, one only needs to audit the correctness of the libraries to be confident in their use. In fact
curve25519_dalek
has been formally
verified when the fiat
backend is used making it objectively
better than many other libraries whose correctness has not been proven. Two additional benefits of the library
choices are simpler APIs making it more likely their use is correct and better cross-platform compatibility.
panic
s related to memory allocations or stack overflow are possible since such issues are not formally guarded against. ↩
Modules§
- bin
bin
- Contains functionality to (de)serialize data to a data store.
- request
- Functionality for starting ceremonies.
- response
- Functionality for completing ceremonies.
Structs§
- Authenticated
Credential - Credential used in authentication ceremonies.
- Discoverable
Authentication Client State - Container of a
DiscoverableCredentialRequestOptions
that has been used to start the authentication ceremony. This gets sent to the client ASAP. - Discoverable
Authentication Server State - State needed to be saved when beginning the authentication ceremony.
- Discoverable
Credential Request Options - The
PublicKeyCredentialRequestOptions
to send to the client when authenticating a credential. - NonDiscoverable
Authentication Client State - Container of a
NonDiscoverableCredentialRequestOptions
that has been used to start the authentication ceremony. This gets sent to the client ASAP. - NonDiscoverable
Authentication Server State - State needed to be saved when beginning the authentication ceremony.
- NonDiscoverable
Credential Request Options - The
PublicKeyCredentialRequestOptions
to send to the client when authenticating a credential. - Public
KeyCredential Creation Options - The
PublicKeyCredentialCreationOptions
to send to the client when registering a new credential. - Registered
Credential - Registered credential that needs to be saved server-side to perform future
authentication ceremonies with
AuthenticatedCredential
. - Registration
PublicKeyCredential
for registration ceremonies.- Registration
Client State - Container of a
PublicKeyCredentialCreationOptions
that has been used to start the registration ceremony. This gets sent to the client ASAP. - Registration
Server State - State needed to be saved when beginning the registration ceremony.
Enums§
- AggErr
- Convenience aggregate error that rolls up all errors into one.
- Credential
Err - Error returned in
RegCeremonyErr::Credential
andAuthCeremonyErr::Credential
as well as fromAuthenticatedCredential::new
.
Type Aliases§
- Discoverable
Authentication Authentication
with a requiredUserHandle
.- Discoverable
Authentication16 Authentication
with a requiredUserHandle16
.- Discoverable
Authentication64 Authentication
with a requiredUserHandle64
.- NonDiscoverable
Authentication Authentication
with an optionalUserHandle
.- NonDiscoverable
Authentication16 Authentication
with an optionalUserHandle16
.- NonDiscoverable
Authentication64 Authentication
with an optionalUserHandle64
.