Source code

Revision control

Copy as Markdown

Other Tools

use super::commands::CommandError;
use crate::transport::errors::HIDError;
use base64::Engine;
use serde::de::{self, Deserializer, Error as SerdeError, MapAccess, Visitor};
use serde::{Deserialize, Serialize, Serializer};
use serde_json as json;
use sha2::{Digest, Sha256};
use std::fmt;
// tokenBinding, of type TokenBinding
//
// This OPTIONAL member contains information about the state of the Token
// Binding protocol [TokenBinding] used when communicating with the Relying
// Party. Its absence indicates that the client doesn’t support token
// binding.
//
// status, of type TokenBindingStatus
//
// This member is one of the following:
//
// supported
//
// Indicates the client supports token binding, but it was not
// negotiated when communicating with the Relying Party.
//
// present
//
// Indicates token binding was used when communicating with the
// Relying Party. In this case, the id member MUST be present.
//
// id, of type DOMString
//
// This member MUST be present if status is present, and MUST be a
// base64url encoding of the Token Binding ID that was used when
// communicating with the Relying Party.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenBinding {
Present(String),
Supported,
}
impl Serialize for TokenBinding {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
TokenBinding::Supported => {
serialize_map!(
serializer,
&"status" => &"supported",
)
}
TokenBinding::Present(ref v) => {
serialize_map!(
serializer,
&"status" => "present",
// Verify here, that `v` is valid base64 encoded?
// base64::decode_config(&v, base64::URL_SAFE_NO_PAD);
// For now: Let the token do that.
&"id" => &v,
)
}
}
}
}
impl<'de> Deserialize<'de> for TokenBinding {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct TokenBindingVisitor;
impl<'de> Visitor<'de> for TokenBindingVisitor {
type Value = TokenBinding;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a byte string")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut id = None;
let mut status = None;
while let Some(key) = map.next_key()? {
match key {
"status" => {
status = Some(map.next_value()?);
}
"id" => {
id = Some(map.next_value()?);
}
k => {
return Err(M::Error::custom(format!("unexpected key: {k:?}")));
}
}
}
if let Some(stat) = status {
match stat {
"present" => {
if let Some(id) = id {
Ok(TokenBinding::Present(id))
} else {
Err(SerdeError::missing_field("id"))
}
}
"supported" => Ok(TokenBinding::Supported),
k => Err(M::Error::custom(format!("unexpected status key: {k:?}"))),
}
} else {
Err(SerdeError::missing_field("status"))
}
}
}
deserializer.deserialize_map(TokenBindingVisitor)
}
}
// type, of type DOMString
//
// This member contains the string "webauthn.create" when creating new
// credentials, and "webauthn.get" when getting an assertion from an
// existing credential. The purpose of this member is to prevent certain
// types of signature confusion attacks (where an attacker substitutes one
// legitimate signature for another).
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum WebauthnType {
Create,
Get,
}
impl Serialize for WebauthnType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
WebauthnType::Create => serializer.serialize_str("webauthn.create"),
WebauthnType::Get => serializer.serialize_str("webauthn.get"),
}
}
}
impl<'de> Deserialize<'de> for WebauthnType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct WebauthnTypeVisitor;
impl<'de> Visitor<'de> for WebauthnTypeVisitor {
type Value = WebauthnType;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match v {
"webauthn.create" => Ok(WebauthnType::Create),
"webauthn.get" => Ok(WebauthnType::Get),
_ => Err(E::custom("unexpected webauthn_type")),
}
}
}
deserializer.deserialize_str(WebauthnTypeVisitor)
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct Challenge(pub String);
impl Challenge {
pub fn new(input: Vec<u8>) -> Self {
let value = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input);
Challenge(value)
}
}
impl From<Vec<u8>> for Challenge {
fn from(v: Vec<u8>) -> Challenge {
Challenge::new(v)
}
}
impl AsRef<[u8]> for Challenge {
fn as_ref(&self) -> &[u8] {
self.0.as_bytes()
}
}
pub type Origin = String;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct CollectedClientData {
#[serde(rename = "type")]
pub webauthn_type: WebauthnType,
pub challenge: Challenge,
pub origin: Origin,
// But we are serializing it, so we *have to* set crossOrigin (if not given, we have to set it to false)
// Thus, on our side, it is not optional. For deserializing, we provide a default (bool's default == False)
#[serde(rename = "crossOrigin", default)]
pub cross_origin: bool,
#[serde(rename = "tokenBinding", skip_serializing_if = "Option::is_none")]
pub token_binding: Option<TokenBinding>,
}
impl CollectedClientData {
pub fn hash(&self) -> Result<ClientDataHash, HIDError> {
// WebIDL's dictionary definition specifies that the order of the struct
// is exactly as the WebIDL specification declares it, with an algorithm
// for partial dictionaries, so that's how interop works for these
// things.
let json = json::to_vec(&self).map_err(CommandError::Json)?;
let digest = Sha256::digest(json);
Ok(ClientDataHash(digest.into()))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClientDataHash(pub [u8; 32]);
impl PartialEq<[u8]> for ClientDataHash {
fn eq(&self, other: &[u8]) -> bool {
self.0.eq(other)
}
}
impl AsRef<[u8]> for ClientDataHash {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl Serialize for ClientDataHash {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_bytes(&self.0)
}
}
#[cfg(test)]
mod test {
use super::{Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType};
use serde_json as json;
#[test]
fn test_token_binding_status() {
let tok = TokenBinding::Present("AAECAw".to_string());
let json_value = json::to_string(&tok).unwrap();
assert_eq!(json_value, "{\"status\":\"present\",\"id\":\"AAECAw\"}");
let tok = TokenBinding::Supported;
let json_value = json::to_string(&tok).unwrap();
assert_eq!(json_value, "{\"status\":\"supported\"}");
}
#[test]
fn test_webauthn_type() {
let t = WebauthnType::Create;
let json_value = json::to_string(&t).unwrap();
assert_eq!(json_value, "\"webauthn.create\"");
let t = WebauthnType::Get;
let json_value = json::to_string(&t).unwrap();
assert_eq!(json_value, "\"webauthn.get\"");
}
#[test]
fn test_collected_client_data_parsing() {
let original_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
let parsed: CollectedClientData = serde_json::from_str(original_str).unwrap();
let expected = CollectedClientData {
webauthn_type: WebauthnType::Create,
challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
origin: String::from("example.com"),
cross_origin: false,
token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
};
assert_eq!(parsed, expected);
let back_again = serde_json::to_string(&expected).unwrap();
assert_eq!(back_again, original_str);
}
#[test]
fn test_collected_client_data_defaults() {
let cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
let no_cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
let parsed: CollectedClientData = serde_json::from_str(no_cross_origin_str).unwrap();
let expected = CollectedClientData {
webauthn_type: WebauthnType::Create,
challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
origin: String::from("example.com"),
cross_origin: false,
token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
};
assert_eq!(parsed, expected);
let back_again = serde_json::to_string(&expected).unwrap();
assert_eq!(back_again, cross_origin_str);
}
#[test]
fn test_collected_client_data() {
let client_data = CollectedClientData {
webauthn_type: WebauthnType::Create,
challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
origin: String::from("example.com"),
cross_origin: false,
token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
};
assert_eq!(
client_data.hash().expect("failed to serialize client data"),
// echo -n '{"type":"webauthn.create","challenge":"AAECAw","origin":"example.com","crossOrigin":false,"tokenBinding":{"status":"present","id":"AAECAw"}}' | sha256sum -t
ClientDataHash([
0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32,
0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87,
0x54, 0xc3, 0x2d, 0x80
])
);
}
}