Skip to content

Commit d5982a0

Browse files
authored
Add PRF extension support (#69)
* Added types for PRF extension * Fixed issue with extensions payload decoding * Fixed issue with extensions payload decoding
1 parent e257919 commit d5982a0

File tree

4 files changed

+168
-39
lines changed

4 files changed

+168
-39
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "slauth"
3-
version = "0.7.4"
3+
version = "0.7.5"
44
authors = ["richer <richer.arc@gmail.com>", "LucFauvel <luc.fauvel@hotmail.com>"]
55
edition = "2021"
66
description = "oath HOTP and TOTP complient implementation"

src/webauthn/proto/raw_message.rs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ use bytes::Buf;
1414
use serde_cbor::Value;
1515
use serde_derive::*;
1616
use std::{
17-
collections::BTreeMap,
1817
io::{Cursor, Read},
1918
str::FromStr,
2019
};
@@ -101,10 +100,13 @@ impl AuthenticatorData {
101100
cursor.read_exact(&mut rp_id_hash)?;
102101

103102
let flags = cursor.read_u8()?;
103+
let has_attested_credential_data = flags & (1 << 6) > 0;
104+
let has_extensions = flags & (1 << 7) > 0;
104105

105106
let sign_count = cursor.read_u32::<BigEndian>()?;
106107

107-
let attested_credential_data = if cursor.remaining() > 16 {
108+
let mut remaining_cbor = Value::Null;
109+
let attested_credential_data = if has_attested_credential_data {
108110
let mut aaguid = [0u8; 16];
109111
cursor.read_exact(&mut aaguid)?;
110112

@@ -115,27 +117,55 @@ impl AuthenticatorData {
115117

116118
let mut remaining = vec![0u8; cursor.remaining()];
117119
cursor.read_exact(&mut remaining[..])?;
120+
let public_key_cbor = match serde_cbor::from_slice::<serde_cbor::Value>(remaining.as_slice()) {
121+
Ok(cred) => cred,
122+
Err(e) if has_extensions && e.is_syntax() => {
123+
// serde_cbor will send a `ErrorImpl` with code: `ErrorCode::TrailingData` and offset: offset of
124+
// first extra byte if we have Extensions blob afterward.
125+
// Since `ErrorImpl` is not public, the best we can do is catch the syntax category and retry
126+
// the slice before the first offset error.
127+
128+
// The offset is incorectly reported as of serde_cbor 0.11.2;
129+
// If, for example, a buffer of 93 bytes contain a valid CBOR payload from [0..77] (77 bytes,
130+
// bytes from 0 to 76 as the 77 bound is exclusive), the reported offset in the error will be 78.
131+
let offset = (e.offset() - 1) as usize;
132+
133+
remaining_cbor = serde_cbor::from_slice::<serde_cbor::Value>(&remaining[offset..])?;
134+
serde_cbor::from_slice::<serde_cbor::Value>(&remaining[..offset])?
135+
}
136+
Err(e) => return Err(Error::CborError(e).into()),
137+
};
118138

119-
let remaining_value = serde_cbor::from_slice::<serde_cbor::Value>(remaining.as_slice()).map_err(Error::CborError)?;
120-
121-
let credential_public_key = CredentialPublicKey::from_value(remaining_value)?;
139+
let credential_public_key = CredentialPublicKey::from_value(&public_key_cbor)?;
122140

123141
Some(AttestedCredentialData {
124142
aaguid,
125143
credential_id,
126144
credential_public_key,
127145
})
128146
} else {
147+
if has_extensions {
148+
let mut remaining = vec![0u8; cursor.remaining()];
149+
cursor.read_exact(&mut remaining[..])?;
150+
remaining_cbor = serde_cbor::from_slice::<serde_cbor::Value>(remaining.as_slice()).map_err(Error::CborError)?;
151+
}
152+
129153
None
130154
};
131155

156+
let extensions = if has_extensions {
157+
remaining_cbor
158+
} else {
159+
Value::Null
160+
};
161+
132162
Ok((
133163
AuthenticatorData {
134164
rp_id_hash,
135165
flags,
136166
sign_count,
137167
attested_credential_data,
138-
extensions: Value::Null,
168+
extensions,
139169
},
140170
cursor.into_inner(),
141171
))
@@ -176,10 +206,10 @@ pub enum CoseKeyInfo {
176206
}
177207

178208
impl CredentialPublicKey {
179-
pub fn from_value(value: serde_cbor::Value) -> Result<Self, Error> {
209+
pub fn from_value(value: &serde_cbor::Value) -> Result<Self, Error> {
180210
let map = match value {
181211
Value::Map(m) => m,
182-
_ => BTreeMap::new(),
212+
_ => return Err(Error::Other("Invalid Cbor for CredentialPublicKey".to_string())),
183213
};
184214

185215
let key_type = map

src/webauthn/proto/web_message.rs

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
use std::collections::HashMap;
12
use serde_derive::*;
2-
use serde_json::Value;
33

4-
#[derive(Serialize, Deserialize, Clone, Debug)]
4+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
55
#[serde(rename = "publicKey", rename_all = "camelCase")]
66
pub struct PublicKeyCredentialCreationOptions {
77
pub rp: PublicKeyCredentialRpEntity,
@@ -16,8 +16,8 @@ pub struct PublicKeyCredentialCreationOptions {
1616
pub authenticator_selection: Option<AuthenticatorSelectionCriteria>,
1717
#[serde(skip_serializing_if = "Option::is_none")]
1818
pub attestation: Option<AttestationConveyancePreference>,
19-
#[serde(skip_serializing_if = "Option::is_none")]
20-
pub extensions: Option<Value>,
19+
#[serde(default, skip_serializing_if = "Extensions::is_empty")]
20+
pub extensions: Extensions,
2121
}
2222

2323
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -32,11 +32,11 @@ pub struct PublicKeyCredentialRequestOptions {
3232
pub allow_credentials: Vec<PublicKeyCredentialDescriptor>,
3333
#[serde(skip_serializing_if = "Option::is_none")]
3434
pub user_verification: Option<UserVerificationRequirement>,
35-
#[serde(skip_serializing_if = "Option::is_none")]
36-
pub extensions: Option<Value>,
35+
#[serde(default, skip_serializing_if = "Extensions::is_empty")]
36+
pub extensions: Extensions,
3737
}
3838

39-
#[derive(Serialize, Deserialize, Clone, Debug)]
39+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
4040
pub struct PublicKeyCredentialRpEntity {
4141
#[serde(skip_serializing_if = "Option::is_none")]
4242
pub id: Option<String>,
@@ -45,7 +45,7 @@ pub struct PublicKeyCredentialRpEntity {
4545
pub icon: Option<String>,
4646
}
4747

48-
#[derive(Serialize, Deserialize, Clone, Debug)]
48+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
4949
#[serde(rename_all = "camelCase")]
5050
pub struct PublicKeyCredentialUserEntity {
5151
pub id: String,
@@ -55,14 +55,14 @@ pub struct PublicKeyCredentialUserEntity {
5555
pub icon: Option<String>,
5656
}
5757

58-
#[derive(Serialize, Deserialize, Clone, Debug)]
58+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
5959
pub struct PublicKeyCredentialParameters {
6060
#[serde(rename = "type")]
6161
pub auth_type: PublicKeyCredentialType,
6262
pub alg: i64,
6363
}
6464

65-
#[derive(Serialize, Deserialize, Clone, Debug)]
65+
#[derive(Serialize, Deserialize, Clone, Debug, Eq)]
6666
pub struct PublicKeyCredentialDescriptor {
6767
#[serde(rename = "type")]
6868
pub cred_type: PublicKeyCredentialType,
@@ -77,13 +77,13 @@ impl PartialEq for PublicKeyCredentialDescriptor {
7777
}
7878
}
7979

80-
#[derive(Serialize, Deserialize, Clone, Debug)]
80+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
8181
pub enum PublicKeyCredentialType {
8282
#[serde(rename = "public-key")]
8383
PublicKey,
8484
}
8585

86-
#[derive(Serialize, Deserialize, Clone, Debug)]
86+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
8787
pub enum AuthenticatorTransport {
8888
#[serde(rename = "usb")]
8989
Usb,
@@ -95,7 +95,7 @@ pub enum AuthenticatorTransport {
9595
Internal,
9696
}
9797

98-
#[derive(Serialize, Deserialize, Clone, Debug)]
98+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
9999
#[serde(rename_all = "camelCase")]
100100
pub struct AuthenticatorSelectionCriteria {
101101
#[serde(skip_serializing_if = "Option::is_none")]
@@ -106,7 +106,7 @@ pub struct AuthenticatorSelectionCriteria {
106106
pub user_verification: Option<UserVerificationRequirement>,
107107
}
108108

109-
#[derive(Serialize, Deserialize, Clone, Debug)]
109+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
110110
#[serde(rename_all = "camelCase")]
111111
pub enum AuthenticatorAttachment {
112112
Platform,
@@ -122,7 +122,7 @@ pub enum UserVerificationRequirement {
122122
Discouraged,
123123
}
124124

125-
#[derive(Serialize, Deserialize, Clone, Debug)]
125+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
126126
#[serde(rename_all = "camelCase")]
127127
pub enum AttestationConveyancePreference {
128128
None,
@@ -178,3 +178,37 @@ pub enum TokenBindingStatus {
178178
Present,
179179
Supported,
180180
}
181+
182+
#[derive(Serialize, Deserialize, Clone, Debug, Default, Eq, PartialEq)]
183+
#[serde(rename_all = "camelCase")]
184+
pub struct Extensions {
185+
#[serde(skip_serializing_if = "Option::is_none")]
186+
pub prf: Option<PrfExtension>,
187+
}
188+
189+
impl Extensions {
190+
pub fn is_empty(&self) -> bool {
191+
self.prf.is_none()
192+
}
193+
}
194+
195+
// https://w3c.github.io/webauthn/#dictdef-authenticationextensionsprfinputs
196+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
197+
#[serde(rename_all = "camelCase")]
198+
pub struct PrfExtension {
199+
#[serde(default, skip_serializing_if = "Option::is_none")]
200+
pub eval: Option<AuthenticationExtensionsPRFValues>,
201+
202+
// Only supported in authentication, not creation
203+
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
204+
pub eval_by_credential: HashMap<String, AuthenticationExtensionsPRFValues>,
205+
}
206+
207+
// https://w3c.github.io/webauthn/#dictdef-authenticationextensionsprfvalues
208+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
209+
#[serde(rename_all = "camelCase")]
210+
pub struct AuthenticationExtensionsPRFValues {
211+
pub first: Vec<u8>,
212+
#[serde(default, skip_serializing_if = "Option::is_none")]
213+
pub second: Option<Vec<u8>>,
214+
}

0 commit comments

Comments
 (0)