Skip to main content

defuse_webauthn/
lib.rs

1use defuse_digest::{Digest, sha2::Sha256};
2use serde::{Deserialize, Serialize, de::DeserializeOwned};
3use serde_with::{
4    base64::{Base64, UrlSafe},
5    formats::Unpadded,
6    serde_as,
7};
8
9#[cfg(feature = "ed25519")]
10mod ed25519;
11#[cfg(feature = "ed25519")]
12pub use self::ed25519::*;
13
14#[cfg(feature = "p256")]
15mod p256;
16#[cfg(feature = "p256")]
17pub use self::p256::*;
18
19#[serde_as]
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[cfg_attr(feature = "abi", derive(::schemars::JsonSchema))]
22#[serde(bound(
23    serialize = "<A as Algorithm>::Signature: Serialize",
24    deserialize = "<A as Algorithm>::Signature: DeserializeOwned",
25))]
26pub struct PayloadSignature<A: Algorithm + ?Sized> {
27    /// Base64Url-encoded [authenticatorData](https://w3c.github.io/webauthn/#authenticator-data)
28    #[serde_as(as = "Base64<UrlSafe, Unpadded>")]
29    pub authenticator_data: Vec<u8>,
30    /// Serialized [clientDataJSON](https://w3c.github.io/webauthn/#dom-authenticatorresponse-clientdatajson)
31    pub client_data_json: String,
32
33    // schemars@0.8 does not respect it's `schemars(bound = "...")`
34    // attribute: https://github.com/GREsau/schemars/blob/104b0fd65055d4b46f8dcbe38cdd2ef2c4098fe2/schemars_derive/src/lib.rs#L193-L206
35    #[cfg_attr(feature = "abi", schemars(with = "String"))]
36    pub signature: A::Signature,
37}
38
39impl<A: Algorithm + ?Sized> PayloadSignature<A> {
40    /// <https://w3c.github.io/webauthn/#sctn-verifying-assertion>
41    ///
42    /// Credits to:
43    /// * [ERC-4337 Smart Wallet](https://github.com/passkeys-4337/smart-wallet/blob/f3aa9fd44646fde0316fc810e21cc553a9ed73e0/contracts/src/WebAuthn.sol#L75-L172)
44    /// * [CAP-0051](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0051.md)
45    pub fn verify(
46        &self,
47        message: impl AsRef<[u8]>,
48        public_key: &A::PublicKey,
49        user_verification: UserVerification,
50    ) -> bool {
51        // verify authData flags
52        if self.authenticator_data.len() < 37
53            || !Self::verify_flags(self.authenticator_data[32], user_verification)
54        {
55            return false;
56        }
57
58        // 10. Verify that the value of C.type is the string webauthn.get.
59        let Ok(c) = serde_json::from_str::<CollectedClientData>(&self.client_data_json) else {
60            return false;
61        };
62        if c.typ != ClientDataType::Get {
63            return false;
64        }
65
66        // 11. Verify that the value of C.challenge equals the base64url
67        // encoding of pkOptions.challenge
68        if c.challenge != message.as_ref() {
69            return false;
70        }
71
72        // 20. Let hash be the result of computing a hash over the cData using
73        // SHA-256
74        let hash = Sha256::digest(self.client_data_json.as_bytes());
75
76        // 21. Using credentialRecord.publicKey, verify that sig is a valid
77        // signature over the binary concatenation of authData and hash.
78        A::verify(
79            &[self.authenticator_data.as_slice(), hash.as_ref()].concat(),
80            public_key,
81            &self.signature,
82        )
83    }
84
85    #[allow(clippy::identity_op)]
86    const AUTH_DATA_FLAGS_UP: u8 = 1 << 0;
87    const AUTH_DATA_FLAGS_UV: u8 = 1 << 2;
88    const AUTH_DATA_FLAGS_BE: u8 = 1 << 3;
89    const AUTH_DATA_FLAGS_BS: u8 = 1 << 4;
90
91    /// <https://w3c.github.io/webauthn/#sctn-verifying-assertion>
92    const fn verify_flags(flags: u8, user_verification: UserVerification) -> bool {
93        // 16. Verify that the UP bit of the flags in authData is set.
94        if flags & Self::AUTH_DATA_FLAGS_UP != Self::AUTH_DATA_FLAGS_UP {
95            return false;
96        }
97
98        // 17. If user verification was determined to be required, verify that
99        // the UV bit of the flags in authData is set. Otherwise, ignore the
100        // value of the UV flag.
101        if user_verification.is_required()
102            && (flags & Self::AUTH_DATA_FLAGS_UV != Self::AUTH_DATA_FLAGS_UV)
103        {
104            return false;
105        }
106
107        // 18. If the BE bit of the flags in authData is not set, verify that
108        // the BS bit is not set.
109        if (flags & Self::AUTH_DATA_FLAGS_BE != Self::AUTH_DATA_FLAGS_BE)
110            && (flags & Self::AUTH_DATA_FLAGS_BS == Self::AUTH_DATA_FLAGS_BS)
111        {
112            return false;
113        }
114
115        true
116    }
117}
118
119#[derive(Debug, Clone, Copy)]
120pub enum UserVerification {
121    Ignore,
122    Require,
123}
124
125impl UserVerification {
126    #[inline]
127    pub const fn is_required(&self) -> bool {
128        matches!(self, Self::Require)
129    }
130}
131
132/// See <https://www.iana.org/assignments/cose/cose.xhtml#algorithms>
133pub trait Algorithm {
134    type PublicKey;
135    type Signature;
136
137    fn verify(msg: &[u8], public_key: &Self::PublicKey, signature: &Self::Signature) -> bool;
138}
139
140/// For more details, refer to [WebAuthn specification](https://w3c.github.io/webauthn/#dictdef-collectedclientdata).
141#[serde_as]
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[cfg_attr(feature = "abi", derive(::schemars::JsonSchema))]
144pub struct CollectedClientData {
145    #[serde(rename = "type")]
146    pub typ: ClientDataType,
147
148    #[serde_as(as = "Base64<UrlSafe, Unpadded>")]
149    pub challenge: Vec<u8>,
150
151    pub origin: String,
152}
153
154#[serde_as]
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156#[cfg_attr(feature = "abi", derive(::schemars::JsonSchema))]
157pub enum ClientDataType {
158    /// Serializes to the string `"webauthn.create"`
159    #[serde(rename = "webauthn.create")]
160    Create,
161
162    /// Serializes to the string `"webauthn.get"`
163    #[serde(rename = "webauthn.get")]
164    Get,
165}