Conversation
|
This now covers both MC and GA requests, but not responses. I'll probably tidy this up and keep responses for a separate PR. Note to self, the failures are due to Base64UrlString being used in CTAP models, which are serialized as a Base64 string rather than URL with serde_cbor. The next step is to separate these models into two, a WebAuthn JSON model using Base64UrlString, and an equivalent CTAP model using ByteBuf. Must also review all usages of Base64UrlString. |
efa24cd to
c117995
Compare
6823563 to
fbd5fb8
Compare
|
Marking this as ready for review as this is getting large, keeping track of further work in #134. |
There was a problem hiding this comment.
Pull Request Overview
This PR introduces the foundation for Web IDL support by implementing basic parsing for GetAssertion (GA) and MakeCredential (MC) operations. The changes enable the library to parse WebAuthn operations from JSON format, which is a key requirement for Web IDL compliance.
- Introduces a new IDL module with JSON parsing capabilities for WebAuthn operations
- Refactors extension handling to use proper typed structures instead of enums
- Adds support for parsing WebAuthn requests from JSON format with proper validation
Reviewed Changes
Copilot reviewed 24 out of 25 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| libwebauthn/src/ops/webauthn/mod.rs | Adds IDL module exports and updates core WebAuthn type definitions |
| libwebauthn/src/ops/webauthn/idl/* | New IDL parsing infrastructure with JSON deserialization support |
| libwebauthn/src/ops/webauthn/make_credential.rs | Refactors extension handling and adds JSON parsing support |
| libwebauthn/src/ops/webauthn/get_assertion.rs | Updates extension structures and adds JSON parsing capabilities |
| libwebauthn/src/proto/ctap2/model/* | Updates extension handling to work with new typed structures |
| libwebauthn/src/tests/basic_ctap2.rs | Updates test to use new extension format |
| Examples and other files | Updates to use new extension APIs |
Comments suppressed due to low confidence (1)
libwebauthn/src/proto/ctap2/model/make_credential.rs:1
- The
Defaulttrait was removed fromCtap2GetAssertionRequestExtensionsbut line 142 in the same file still callsskip_serializing_extensionswhich expects a default value check. This could cause compilation issues.
use super::{
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| impl Into<String> for RelyingPartyId { | ||
| fn into(self) -> String { | ||
| self.0 |
There was a problem hiding this comment.
Consider implementing From<RelyingPartyId> for String instead of Into<String> for RelyingPartyId. The From trait automatically provides the Into implementation and is the preferred approach in Rust.
| impl Into<String> for RelyingPartyId { | |
| fn into(self) -> String { | |
| self.0 | |
| impl From<RelyingPartyId> for String { | |
| fn from(rpid: RelyingPartyId) -> String { | |
| rpid.0 |
| impl Into<Ctap2PublicKeyCredentialDescriptor> for PublicKeyCredentialDescriptorJSON { | ||
| fn into(self) -> Ctap2PublicKeyCredentialDescriptor { | ||
| Ctap2PublicKeyCredentialDescriptor { | ||
| r#type: self.r#type, | ||
| id: ByteBuf::from(self.id), | ||
| transports: self.transports, |
There was a problem hiding this comment.
Consider implementing From<PublicKeyCredentialDescriptorJSON> for Ctap2PublicKeyCredentialDescriptor instead of Into. The From trait automatically provides the Into implementation and is the preferred approach in Rust.
| impl Into<Ctap2PublicKeyCredentialDescriptor> for PublicKeyCredentialDescriptorJSON { | |
| fn into(self) -> Ctap2PublicKeyCredentialDescriptor { | |
| Ctap2PublicKeyCredentialDescriptor { | |
| r#type: self.r#type, | |
| id: ByteBuf::from(self.id), | |
| transports: self.transports, | |
| impl From<PublicKeyCredentialDescriptorJSON> for Ctap2PublicKeyCredentialDescriptor { | |
| fn from(value: PublicKeyCredentialDescriptorJSON) -> Self { | |
| Ctap2PublicKeyCredentialDescriptor { | |
| r#type: value.r#type, | |
| id: ByteBuf::from(value.id), | |
| transports: value.transports, |
| impl Into<Vec<u8>> for Base64UrlString { | ||
| fn into(self) -> Vec<u8> { | ||
| self.0 |
There was a problem hiding this comment.
Consider implementing From<Base64UrlString> for Vec<u8> instead of Into<Vec<u8>>. The From trait automatically provides the Into implementation and is the preferred approach in Rust.
| impl Into<Vec<u8>> for Base64UrlString { | |
| fn into(self) -> Vec<u8> { | |
| self.0 | |
| impl From<Base64UrlString> for Vec<u8> { | |
| fn from(b64: Base64UrlString) -> Vec<u8> { | |
| b64.0 |
msirringhaus
left a comment
There was a problem hiding this comment.
Looking good overall, I think. I haven't had time for a full deep dive, though. I only have some preliminary questions.
| (signed_extensions.hmac_secret, None) | ||
| } | ||
| MakeCredentialHmacOrPrfInput::Prf => ( | ||
| if let Some(_hmac_create_secret) = incoming_ext.hmac_create_secret { |
There was a problem hiding this comment.
Using if {} else if {} here means you can either have HMAC or PRF, and if you request both, HMAC will override PRF. Is that intended?
There was a problem hiding this comment.
Hmm, I think you're right. Whilst I wouldn't expect a browser to request this, I'll change it so that if your makeCredential includes both HMAC & PRF, you will get both responses back.
|
|
||
| use super::{DowngradableRequest, RegisterRequest, UserVerificationRequirement}; | ||
|
|
||
| pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); |
There was a problem hiding this comment.
Should we use the defintion from libwebauthn/src/ops/webauthn/timeout.rs here? (Same in the other files where this is defined again)
| pub hash: Vec<u8>, | ||
| pub allow: Vec<Ctap2PublicKeyCredentialDescriptor>, | ||
| pub extensions: Option<GetAssertionRequestExtensions>, | ||
| pub extensions: GetAssertionRequestExtensions, |
There was a problem hiding this comment.
I confess, I'm currently a bit lost in the details of this huge PR, but if I recall correctly, we used to use the Option<> here to decide if we have to include an extensions-map in the output. Some portals like demo.yubico.com are rather strict on what they expect, e.g. if they request extensions, they expect extensions in the response, even if the map is empty. And they do not want to see that empty map, if they didn't request one.
Is this still possible with this change?
There was a problem hiding this comment.
Thanks for spotting this, addressing it so we can represent both states. Also adding a test for both cases (extensions: {} and no etensions).
fbb40b2 to
39cca52
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let resident_key = if inner | ||
| .authenticator_selection | ||
| .as_ref() | ||
| .and_then(|s| Some(s.require_resident_key)) |
There was a problem hiding this comment.
The .and_then(|s| Some(s.require_resident_key)) is incorrect. Using and_then with a closure that always returns Some is redundant - this should be .map(|s| s.require_resident_key) instead.
| .and_then(|s| Some(s.require_resident_key)) | |
| .map(|s| s.require_resident_key) |
| let hmac_or_prf = match inner.extensions.clone() { | ||
| Some(ext) => { | ||
| if let Some(prf) = ext.prf { | ||
| let prf_input = PrfInput::try_from(prf)?; | ||
| Some(GetAssertionHmacOrPrfInput::Prf(prf_input)) | ||
| } else if let Some(hmac) = ext.hamc_get_secret { | ||
| let hmac_input = HMACGetSecretInput::try_from(hmac)?; | ||
| Some(GetAssertionHmacOrPrfInput::HmacGetSecret(hmac_input)) | ||
| } else { | ||
| None | ||
| } | ||
| } | ||
| None => None, | ||
| }; |
There was a problem hiding this comment.
The .clone() on line 84 followed by accessing .as_ref() on line 102 is inefficient. The extensions should only be accessed once (using .as_ref()) and then the necessary fields cloned individually if needed.
| let mut channel = device.channel().await?; | ||
| channel.wink(TIMEOUT).await?; | ||
|
|
||
| // Relying |
There was a problem hiding this comment.
The comment "// Relying" on line 79 appears to be incomplete. It should probably say "// Relying Party ID" or similar.
| // Relying | |
| // Relying Party ID |
| if let Some(prf) = ext.prf { | ||
| let prf_input = PrfInput::try_from(prf)?; | ||
| Some(GetAssertionHmacOrPrfInput::Prf(prf_input)) | ||
| } else if let Some(hmac) = ext.hamc_get_secret { |
There was a problem hiding this comment.
The field name hamc_get_secret should be hmac_get_secret (typo is propagated from the struct definition). This will cause the extension to be silently ignored rather than parsed, which is a critical bug.
| } else if let Some(hmac) = ext.hamc_get_secret { | |
| } else if let Some(hmac) = ext.hmac_get_secret { |
| #[serde(rename = "largeBlobKey")] | ||
| pub large_blob: Option<LargeBlobInputJson>, | ||
| #[serde(rename = "hmacCreateSecret")] | ||
| pub hamc_get_secret: Option<HmacGetSecretInputJson>, |
There was a problem hiding this comment.
Typo in the field name: "hamc_get_secret" should be "hmac_get_secret" (missing the 'a' in 'hmac').
| pub hamc_get_secret: Option<HmacGetSecretInputJson>, | |
| pub hmac_get_secret: Option<HmacGetSecretInputJson>, |
msirringhaus
left a comment
There was a problem hiding this comment.
Looking good so far as I can tell. A few nitpicks below.
iinuwa
left a comment
There was a problem hiding this comment.
This is getting pretty big. I think there are some good improvements here, and I think it'd be good to start using this to shake out the bugs.
If the easily fixable comments can be addressed, the rest can be addressed in a follow up PR to keep this moving. (e.g., my comments about PRF can be fixed in a subsequent PR.)
| impl Into<Vec<u8>> for Base64UrlString { | ||
| fn into(self) -> Vec<u8> { | ||
| self.0 |
| impl Into<Ctap2PublicKeyCredentialDescriptor> for PublicKeyCredentialDescriptorJSON { | ||
| fn into(self) -> Ctap2PublicKeyCredentialDescriptor { | ||
| Ctap2PublicKeyCredentialDescriptor { | ||
| r#type: self.r#type, | ||
| id: ByteBuf::from(self.id), | ||
| transports: self.transports, |
| impl Into<String> for RelyingPartyId { | ||
| fn into(self) -> String { | ||
| self.0 |
- Rename 'uv_requirement' to 'user_verification' to match WebAuthn IDL - Add Default derive for UserVerificationRequirement (required by serde) - Update references in get_assertion.rs
|
Thank you both. Addressed all comments in your last review. Also opened #166 for @iinuwa's comment as the change is larger. As per discussion on #linux-credentials:matrix.org:
|
No description provided.