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