Skip to content

Commit d3c31bb

Browse files
feat(idl): Add WebAuthn response JSON serialization
This adds the inverse of JSON request parsing - serializing WebAuthn responses back to JSON format per WebAuthn Level 3 specification. Changes: - Add WebAuthnIDLResponse trait for response-to-JSON conversion - Add RegistrationResponseJSON and AuthenticationResponseJSON models - Implement to_json() for MakeCredentialResponse and Assertion - Refactor requests to store challenge/origin/cross_origin separately - Add client_data_hash() and client_data_json() helper methods - Update all examples and tests to use new request structure - Add unit tests for response serialization
1 parent fbb40b2 commit d3c31bb

18 files changed

+999
-82
lines changed

libwebauthn/examples/prf_test.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ async fn run_success_test(
152152
) {
153153
let get_assertion = GetAssertionRequest {
154154
relying_party_id: "demo.yubico.com".to_owned(),
155-
hash: Vec::from(challenge),
155+
challenge: Vec::from(challenge),
156+
origin: "demo.yubico.com".to_string(),
157+
cross_origin: None,
156158
allow: vec![credential.clone()],
157159
user_verification: UserVerificationRequirement::Preferred,
158160
extensions: Some(GetAssertionRequestExtensions {

libwebauthn/examples/webauthn_cable.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
120120

121121
// Make Credentials ceremony
122122
let make_credentials_request = MakeCredentialRequest {
123+
challenge: Vec::from(challenge),
123124
origin: "example.org".to_owned(),
124-
hash: Vec::from(challenge),
125+
cross_origin: None,
125126
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
126127
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
127128
resident_key: Some(ResidentKeyRequirement::Discouraged),
@@ -159,7 +160,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
159160

160161
let get_assertion = GetAssertionRequest {
161162
relying_party_id: "example.org".to_owned(),
162-
hash: Vec::from(challenge),
163+
challenge: Vec::from(challenge),
164+
origin: "example.org".to_string(),
165+
cross_origin: None,
163166
allow: vec![credential],
164167
user_verification: UserVerificationRequirement::Discouraged,
165168
extensions: Some(GetAssertionRequestExtensions::default()),

libwebauthn/examples/webauthn_extensions_hid.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
105105

106106
// Make Credentials ceremony
107107
let make_credentials_request = MakeCredentialRequest {
108+
challenge: Vec::from(challenge),
108109
origin: "example.org".to_owned(),
109-
hash: Vec::from(challenge),
110+
cross_origin: None,
110111
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
111112
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
112113
resident_key: Some(ResidentKeyRequirement::Required),
@@ -144,7 +145,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
144145
(&response.authenticator_data).try_into().unwrap();
145146
let get_assertion = GetAssertionRequest {
146147
relying_party_id: "example.org".to_owned(),
147-
hash: Vec::from(challenge),
148+
challenge: Vec::from(challenge),
149+
origin: "example.org".to_string(),
150+
cross_origin: None,
148151
allow: vec![credential],
149152
user_verification: UserVerificationRequirement::Discouraged,
150153
extensions: Some(GetAssertionRequestExtensions {

libwebauthn/examples/webauthn_hid.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
8989

9090
// Make Credentials ceremony
9191
let make_credentials_request = MakeCredentialRequest {
92+
challenge: Vec::from(challenge),
9293
origin: "example.org".to_owned(),
93-
hash: Vec::from(challenge),
94+
cross_origin: None,
9495
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
9596
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
9697
resident_key: Some(ResidentKeyRequirement::Discouraged),
@@ -127,7 +128,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
127128
(&response.authenticator_data).try_into().unwrap();
128129
let get_assertion = GetAssertionRequest {
129130
relying_party_id: "example.org".to_owned(),
130-
hash: Vec::from(challenge),
131+
challenge: Vec::from(challenge),
132+
origin: "example.org".to_string(),
133+
cross_origin: None,
131134
allow: vec![credential],
132135
user_verification: UserVerificationRequirement::Discouraged,
133136
extensions: Some(GetAssertionRequestExtensions::default()),

libwebauthn/examples/webauthn_json_hid.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use tokio::sync::broadcast::Receiver;
88
use tracing_subscriber::{self, EnvFilter};
99

1010
use libwebauthn::ops::webauthn::{
11-
GetAssertionRequest, MakeCredentialRequest, RelyingPartyId, WebAuthnIDL as _,
11+
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RelyingPartyId,
12+
WebAuthnIDL as _, WebAuthnIDLResponse as _,
1213
};
1314
use libwebauthn::pin::PinRequestReason;
1415
use libwebauthn::transport::hid::list_devices;
@@ -132,6 +133,20 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
132133
.unwrap();
133134
println!("WebAuthn MakeCredential response: {:?}", response);
134135

136+
// Serialize the response back to JSON using the original request as context.
137+
// The request contains the client_data_json bytes that were built during parsing.
138+
match response.to_json(&make_credentials_request, JsonFormat::Prettified) {
139+
Ok(response_json) => {
140+
println!(
141+
"WebAuthn MakeCredential response (JSON):\n{}",
142+
response_json
143+
);
144+
}
145+
Err(e) => {
146+
eprintln!("Failed to serialize MakeCredential response: {:?}", e);
147+
}
148+
}
149+
135150
let request_json = r#"
136151
{
137152
"challenge": "Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu",
@@ -160,6 +175,18 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
160175
}
161176
.unwrap();
162177
println!("WebAuthn GetAssertion response: {:?}", response);
178+
179+
// Serialize the response back to JSON using the original request as context.
180+
for assertion in &response.assertions {
181+
match assertion.to_json(&get_assertion, JsonFormat::Prettified) {
182+
Ok(assertion_json) => {
183+
println!("WebAuthn GetAssertion response (JSON):\n{}", assertion_json);
184+
}
185+
Err(e) => {
186+
eprintln!("Failed to serialize GetAssertion response: {:?}", e);
187+
}
188+
}
189+
}
163190
}
164191

165192
Ok(())

libwebauthn/examples/webauthn_preflight_hid.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,9 @@ async fn make_credential_call(
162162
) -> Result<Ctap2PublicKeyCredentialDescriptor, WebAuthnError> {
163163
let challenge: [u8; 32] = thread_rng().gen();
164164
let make_credentials_request = MakeCredentialRequest {
165+
challenge: Vec::from(challenge),
165166
origin: "example.org".to_owned(),
166-
hash: Vec::from(challenge),
167+
cross_origin: None,
167168
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
168169
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
169170
resident_key: Some(ResidentKeyRequirement::Discouraged),
@@ -200,7 +201,9 @@ async fn get_assertion_call(
200201
let challenge: [u8; 32] = thread_rng().gen();
201202
let get_assertion = GetAssertionRequest {
202203
relying_party_id: "example.org".to_owned(),
203-
hash: Vec::from(challenge),
204+
challenge: Vec::from(challenge),
205+
origin: "example.org".to_string(),
206+
cross_origin: None,
204207
allow: allow_list,
205208
user_verification: UserVerificationRequirement::Discouraged,
206209
extensions: Some(GetAssertionRequestExtensions::default()),

libwebauthn/examples/webauthn_prf_hid.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use tracing_subscriber::{self, EnvFilter};
1313

1414
use libwebauthn::ops::webauthn::{
1515
GetAssertionHmacOrPrfInput, GetAssertionRequest, GetAssertionRequestExtensions,
16-
MakeCredentialPrfInput, MakeCredentialRequest, MakeCredentialsRequestExtensions, PRFValue,
17-
PrfInput, ResidentKeyRequirement, UserVerificationRequirement,
16+
MakeCredentialPrfInput, MakeCredentialRequest, MakeCredentialsRequestExtensions,
17+
PRFValue, PrfInput, ResidentKeyRequirement, UserVerificationRequirement,
1818
};
1919
use libwebauthn::pin::PinRequestReason;
2020
use libwebauthn::proto::ctap2::{
@@ -99,8 +99,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
9999

100100
// Make Credentials ceremony
101101
let make_credentials_request = MakeCredentialRequest {
102+
challenge: Vec::from(challenge),
102103
origin: "example.org".to_owned(),
103-
hash: Vec::from(challenge),
104+
cross_origin: None,
104105
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
105106
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
106107
resident_key: Some(ResidentKeyRequirement::Required),
@@ -423,7 +424,9 @@ async fn run_success_test(
423424
) {
424425
let get_assertion = GetAssertionRequest {
425426
relying_party_id: "example.org".to_owned(),
426-
hash: Vec::from(challenge),
427+
challenge: Vec::from(challenge),
428+
origin: "example.org".to_string(),
429+
cross_origin: None,
427430
allow: vec![credential.clone()],
428431
user_verification: UserVerificationRequirement::Discouraged,
429432
extensions: Some(GetAssertionRequestExtensions {
@@ -465,7 +468,9 @@ async fn run_failed_test(
465468
) {
466469
let get_assertion = GetAssertionRequest {
467470
relying_party_id: "example.org".to_owned(),
468-
hash: Vec::from(challenge),
471+
challenge: Vec::from(challenge),
472+
origin: "example.org".to_string(),
473+
cross_origin: None,
469474
allow: credential.map(|x| vec![x.clone()]).unwrap_or_default(),
470475
user_verification: UserVerificationRequirement::Discouraged,
471476
extensions: Some(GetAssertionRequestExtensions {

libwebauthn/src/ops/u2f.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use x509_parser::nom::AsBytes;
99
use super::webauthn::MakeCredentialRequest;
1010
use crate::fido::{AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags};
1111
use crate::ops::webauthn::{
12-
GetAssertionRequest, GetAssertionResponse,
13-
MakeCredentialResponse, UserVerificationRequirement,
12+
GetAssertionRequest, GetAssertionResponse, MakeCredentialResponse,
13+
UserVerificationRequirement,
1414
};
1515
use crate::proto::ctap1::{Ctap1RegisterRequest, Ctap1SignRequest};
1616
use crate::proto::ctap1::{Ctap1RegisterResponse, Ctap1SignResponse};
@@ -133,7 +133,7 @@ impl UpgradableResponse<MakeCredentialResponse, MakeCredentialRequest> for Regis
133133
// states a different length range.
134134
let attestation_statement = Ctap2AttestationStatement::FidoU2F(FidoU2fAttestationStmt {
135135
signature: ByteBuf::from(self.signature.clone()),
136-
certificate: ByteBuf::from(self.attestation.clone()),
136+
certificates: vec![ByteBuf::from(self.attestation.clone())],
137137
});
138138

139139
// Let attestationObject be a CBOR map (see "attObj" in Generating an Attestation Object [WebAuthn]) with the
@@ -201,7 +201,9 @@ impl UpgradableResponse<GetAssertionResponse, SignRequest> for SignResponse {
201201
// something like that here. In reality, we only need `extensions: None` currently.
202202
let orig_request = GetAssertionRequest {
203203
relying_party_id: String::new(), // We don't have access to that info here, but we don't need it either
204-
hash: request.app_id_hash.clone(),
204+
challenge: Vec::new(), // U2F path doesn't use client_data for response serialization
205+
origin: String::new(),
206+
cross_origin: None,
205207
allow: vec![Ctap2PublicKeyCredentialDescriptor {
206208
r#type: Ctap2PublicKeyCredentialType::PublicKey,
207209
id: request.key_handle.clone().into(),

libwebauthn/src/ops/webauthn/client_data.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ pub struct ClientData {
1313
}
1414

1515
impl ClientData {
16-
pub fn hash(&self) -> Vec<u8> {
16+
/// Returns the canonical JSON representation of the client data.
17+
pub fn to_json_bytes(&self) -> Vec<u8> {
1718
let op_str = match &self.operation {
1819
Operation::MakeCredential => "webauthn.create",
1920
Operation::GetAssertion => "webauthn.get",
@@ -25,11 +26,13 @@ impl ClientData {
2526
} else {
2627
"false"
2728
};
28-
let json =
29-
format!("{{\"type\":\"{op_str}\",\"challenge\":\"{challenge_str}\",\"origin\":\"{origin_str}\",\"crossOrigin\":{cross_origin_str}}}");
29+
format!("{{\"type\":\"{op_str}\",\"challenge\":\"{challenge_str}\",\"origin\":\"{origin_str}\",\"crossOrigin\":{cross_origin_str}}}").into_bytes()
30+
}
3031

32+
pub fn hash(&self) -> Vec<u8> {
33+
let json_bytes = self.to_json_bytes();
3134
let mut hasher = Sha256::new();
32-
hasher.update(json.as_bytes());
35+
hasher.update(&json_bytes);
3336
hasher.finalize().to_vec()
3437
}
3538
}

0 commit comments

Comments
 (0)