defuse_webauthn/
lib.rs

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