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