defuse_webauthn/
lib.rs

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