Skip to content

Commit db62257

Browse files
Make Credential parsing seems working; added example
1 parent f64a4f1 commit db62257

File tree

6 files changed

+207
-22
lines changed

6 files changed

+207
-22
lines changed

Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use std::convert::TryInto;
2+
use std::error::Error;
3+
use std::io::{self, Write};
4+
use std::time::Duration;
5+
6+
use ctap_types::ctap2::make_credential;
7+
use libwebauthn::UvUpdate;
8+
use rand::{thread_rng, Rng};
9+
use text_io::read;
10+
use tokio::sync::broadcast::Receiver;
11+
use tracing_subscriber::{self, EnvFilter};
12+
13+
use libwebauthn::ops::webauthn::{
14+
GetAssertionRequest, MakeCredentialRequest, RelyingPartyId, ResidentKeyRequirement,
15+
UserVerificationRequirement, WebAuthnIDL as _,
16+
};
17+
use libwebauthn::pin::PinRequestReason;
18+
use libwebauthn::proto::ctap2::{
19+
Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
20+
Ctap2PublicKeyCredentialUserEntity,
21+
};
22+
use libwebauthn::transport::hid::list_devices;
23+
use libwebauthn::transport::{Channel as _, Device};
24+
use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn};
25+
26+
const TIMEOUT: Duration = Duration::from_secs(10);
27+
28+
fn setup_logging() {
29+
tracing_subscriber::fmt()
30+
.with_env_filter(EnvFilter::from_default_env())
31+
.without_time()
32+
.init();
33+
}
34+
35+
async fn handle_updates(mut state_recv: Receiver<UvUpdate>) {
36+
while let Ok(update) = state_recv.recv().await {
37+
match update {
38+
UvUpdate::PresenceRequired => println!("Please touch your device!"),
39+
UvUpdate::UvRetry { attempts_left } => {
40+
print!("UV failed.");
41+
if let Some(attempts_left) = attempts_left {
42+
print!(" You have {attempts_left} attempts left.");
43+
}
44+
}
45+
UvUpdate::PinRequired(update) => {
46+
let mut attempts_str = String::new();
47+
if let Some(attempts) = update.attempts_left {
48+
attempts_str = format!(". You have {attempts} attempts left!");
49+
};
50+
51+
match update.reason {
52+
PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."),
53+
PinRequestReason::AuthenticatorPolicy => {
54+
println!("Your device requires a PIN.")
55+
}
56+
PinRequestReason::FallbackFromUV => {
57+
println!("UV failed too often and is blocked. Falling back to PIN.")
58+
}
59+
}
60+
print!("PIN: Please enter the PIN for your authenticator{attempts_str}: ");
61+
io::stdout().flush().unwrap();
62+
let pin_raw: String = read!("{}\n");
63+
64+
if pin_raw.is_empty() {
65+
println!("PIN: No PIN provided, cancelling operation.");
66+
update.cancel();
67+
} else {
68+
let _ = update.send_pin(&pin_raw);
69+
}
70+
}
71+
}
72+
}
73+
}
74+
75+
#[tokio::main]
76+
pub async fn main() -> Result<(), Box<dyn Error>> {
77+
setup_logging();
78+
79+
let devices = list_devices().await.unwrap();
80+
println!("Devices found: {:?}", devices);
81+
82+
let user_id: [u8; 32] = thread_rng().gen();
83+
let challenge: [u8; 32] = thread_rng().gen();
84+
85+
for mut device in devices {
86+
println!("Selected HID authenticator: {}", &device);
87+
let mut channel = device.channel().await?;
88+
channel.wink(TIMEOUT).await?;
89+
90+
// Relying
91+
let rpid = RelyingPartyId("example.org".to_owned());
92+
let request_json = r#"
93+
{
94+
"rp": {
95+
"id": "example.org",
96+
"name": "Example Relying Party"
97+
},
98+
"user": {
99+
"id": "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzg",
100+
"name": "Mario Rossi",
101+
"displayName": "Mario Rossi"
102+
},
103+
"challenge": "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzg",
104+
"pubKeyCredParams": [
105+
{"type": "public-key", "alg": -7}
106+
],
107+
"timeout": 60000,
108+
"excludeCredentials": [],
109+
"authenticatorSelection": {
110+
"residentKey": "discouraged",
111+
"userVerification": "preferred"
112+
},
113+
"attestation": "none"
114+
}
115+
"#;
116+
let make_credentials_request: MakeCredentialRequest =
117+
MakeCredentialRequest::from_json(&rpid, request_json)
118+
.expect("Failed to parse request JSON");
119+
println!(
120+
"WebAuthn MakeCredential request: {:?}",
121+
make_credentials_request
122+
);
123+
124+
let state_recv = channel.get_ux_update_receiver();
125+
tokio::spawn(handle_updates(state_recv));
126+
127+
let response = loop {
128+
match channel
129+
.webauthn_make_credential(&make_credentials_request)
130+
.await
131+
{
132+
Ok(response) => break Ok(response),
133+
Err(WebAuthnError::Ctap(ctap_error)) => {
134+
if ctap_error.is_retryable_user_error() {
135+
println!("Oops, try again! Error: {}", ctap_error);
136+
continue;
137+
}
138+
break Err(WebAuthnError::Ctap(ctap_error));
139+
}
140+
Err(err) => break Err(err),
141+
};
142+
}
143+
.unwrap();
144+
println!("WebAuthn MakeCredential response: {:?}", response);
145+
146+
let credential: Ctap2PublicKeyCredentialDescriptor =
147+
(&response.authenticator_data).try_into().unwrap();
148+
let get_assertion = GetAssertionRequest {
149+
relying_party_id: "example.org".to_owned(),
150+
hash: Vec::from(challenge),
151+
allow: vec![credential],
152+
user_verification: UserVerificationRequirement::Discouraged,
153+
extensions: None,
154+
timeout: TIMEOUT,
155+
};
156+
157+
let response = loop {
158+
match channel.webauthn_get_assertion(&get_assertion).await {
159+
Ok(response) => break Ok(response),
160+
Err(WebAuthnError::Ctap(ctap_error)) => {
161+
if ctap_error.is_retryable_user_error() {
162+
println!("Oops, try again! Error: {}", ctap_error);
163+
continue;
164+
}
165+
break Err(WebAuthnError::Ctap(ctap_error));
166+
}
167+
Err(err) => break Err(err),
168+
};
169+
}
170+
.unwrap();
171+
println!("WebAuthn GetAssertion response: {:?}", response);
172+
}
173+
174+
Ok(())
175+
}

libwebauthn/src/ops/webauthn/create.rs

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,6 @@ use serde_json::{Map as JsonMap, Value as JsonValue};
1616
* https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON
1717
*/
1818

19-
#[derive(Debug, Clone, Deserialize)]
20-
pub struct PublicKeyCredentialDescriptorJSON {
21-
pub r#type: String,
22-
pub id: Base64UrlString,
23-
pub transports: Vec<String>,
24-
}
25-
26-
#[derive(Debug, Clone, Deserialize)]
27-
pub struct PublicKeyCredentialParameters {
28-
pub r#type: String,
29-
pub alg: i32,
30-
}
31-
3219
#[derive(Debug, Clone, Deserialize)]
3320
pub struct AuthenticatorSelectionCriteria {
3421
#[serde(rename = "authenticatorAttachment")]
@@ -61,9 +48,9 @@ pub struct PublicKeyCredentialCreationOptionsJSON {
6148
pub exclude_credentials: Vec<Ctap2PublicKeyCredentialDescriptor>,
6249
#[serde(rename = "authenticatorSelection")]
6350
pub authenticator_selection: Option<AuthenticatorSelectionCriteria>,
64-
pub hints: Vec<String>,
65-
pub attestation: String,
51+
pub hints: Option<Vec<String>>,
52+
pub attestation: Option<String>,
6653
#[serde(rename = "attestationFormats")]
67-
pub attestation_formats: Vec<String>,
68-
pub extensions: MakeCredentialsRequestExtensions,
54+
pub attestation_formats: Option<Vec<String>>,
55+
pub extensions: Option<MakeCredentialsRequestExtensions>,
6956
}

libwebauthn/src/ops/webauthn/idl.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ impl Deref for Base64UrlString {
5858
}
5959
}
6060

61+
impl AsRef<[u8]> for Base64UrlString {
62+
fn as_ref(&self) -> &[u8] {
63+
&self.0
64+
}
65+
}
66+
6167
impl<'de> Deserialize<'de> for Base64UrlString {
6268
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6369
where

libwebauthn/src/ops/webauthn/make_credential.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ impl FromInnerModel<PublicKeyCredentialCreationOptionsJSON, MakeCredentialReques
231231
user_verification,
232232
algorithms: inner.params,
233233
exclude,
234-
extensions: Some(inner.extensions),
234+
extensions: inner.extensions,
235235
timeout: Duration::from_secs(inner.timeout.into()),
236236
})
237237
}
@@ -242,9 +242,6 @@ pub enum MakeCredentialRequestParsingError {
242242
/// The client must throw an "EncodingError" DOMException.
243243
#[error("Invalid JSON format: {0}")]
244244
EncodingError(#[from] JsonError),
245-
246-
#[error("Invalid extension: {0}")]
247-
ExtensionError(JsonError),
248245
}
249246

250247
impl WebAuthnIDL<MakeCredentialRequestParsingError> for MakeCredentialRequest {

libwebauthn/src/ops/webauthn/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ pub use get_assertion::{
1313
GetAssertionResponseExtensions, GetAssertionResponseUnsignedExtensions, HMACGetSecretInput,
1414
HMACGetSecretOutput, PRFValue,
1515
};
16-
pub use idl::Base64UrlString;
16+
pub use idl::{Base64UrlString, WebAuthnIDL};
1717
pub use make_credential::{
1818
CredentialPropsExtension, CredentialProtectionExtension, CredentialProtectionPolicy,
1919
MakeCredentialLargeBlobExtension, MakeCredentialLargeBlobExtensionOutput,
2020
MakeCredentialPrfInput, MakeCredentialPrfOutput, MakeCredentialRequest, MakeCredentialResponse,
2121
MakeCredentialsRequestExtensions, MakeCredentialsResponseExtensions,
2222
MakeCredentialsResponseUnsignedExtensions, ResidentKeyRequirement,
2323
};
24+
pub use rpid::RelyingPartyId;
2425
use serde::Deserialize;
2526

2627
#[derive(Debug, Clone, Copy, Deserialize)]

0 commit comments

Comments
 (0)