defuse_webauthn/
lib.rs

1use serde::{Deserialize, Serialize, de::DeserializeOwned};
2use serde_with::serde_as;
3
4use defuse_serde_utils::base64::{Base64, Unpadded, UrlSafe};
5
6#[cfg(feature = "ed25519")]
7mod ed25519;
8#[cfg(feature = "ed25519")]
9pub use self::ed25519::*;
10
11#[cfg(feature = "p256")]
12mod p256;
13#[cfg(feature = "p256")]
14pub use self::p256::*;
15
16#[serde_as]
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[cfg_attr(feature = "abi", derive(::schemars::JsonSchema))]
19#[serde(bound(
20    serialize = "<A as Algorithm>::Signature: Serialize",
21    deserialize = "<A as Algorithm>::Signature: DeserializeOwned",
22))]
23pub struct PayloadSignature<A: Algorithm + ?Sized> {
24    /// Base64Url-encoded [authenticatorData](https://w3c.github.io/webauthn/#authenticator-data)
25    #[serde_as(as = "Base64<UrlSafe, Unpadded>")]
26    #[cfg_attr(feature = "abi", schemars(with = "String"))]
27    pub authenticator_data: Vec<u8>,
28    /// Serialized [clientDataJSON](https://w3c.github.io/webauthn/#dom-authenticatorresponse-clientdatajson)
29    pub client_data_json: String,
30
31    // schemars@0.8 does not respect it's `schemars(bound = "...")`
32    // attribute: https://github.com/GREsau/schemars/blob/104b0fd65055d4b46f8dcbe38cdd2ef2c4098fe2/schemars_derive/src/lib.rs#L193-L206
33    #[cfg_attr(feature = "abi", schemars(with = "String"))]
34    pub signature: A::Signature,
35}
36
37#[cfg(any(test, feature = "sha2", feature = "near-contract"))]
38impl<A: Algorithm + ?Sized> PayloadSignature<A> {
39    /// <https://w3c.github.io/webauthn/#sctn-verifying-assertion>
40    ///
41    /// Credits to:
42    /// * [ERC-4337 Smart Wallet](https://github.com/passkeys-4337/smart-wallet/blob/f3aa9fd44646fde0316fc810e21cc553a9ed73e0/contracts/src/WebAuthn.sol#L75-L172)
43    /// * [CAP-0051](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0051.md)
44    pub fn verify(
45        &self,
46        message: impl AsRef<[u8]>,
47        public_key: &A::PublicKey,
48        user_verification: UserVerification,
49    ) -> bool {
50        use defuse_digest::Digest;
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 = defuse_digest::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}