- 
                Notifications
    You must be signed in to change notification settings 
- Fork 19
Web IDL support 1/N: GA, MC basic parsing #138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 | 
    
      
    
      Copilot
AI
    
    
    
      Sep 8, 2025 
    
  
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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, | 
    
      
    
      Copilot
AI
    
    
    
      Sep 8, 2025 
    
  
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 | 
    
      
    
      Copilot
AI
    
    
    
      Sep 8, 2025 
    
  
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 | 
| let json = | ||
| format!("{{\"type\":\"{op_str}\",\"challenge\":\"{challenge_str}\",\"origin\":\"{origin_str}\",\"crossOrigin\":{cross_origin_str}}}"); | 
    
      
    
      Copilot
AI
    
    
    
      Sep 8, 2025 
    
  
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JSON string construction using format! is error-prone and hard to maintain. Consider using serde_json to serialize a proper struct to ensure valid JSON format.
| Some(inner.exclude_credentials) | ||
| }, | ||
| extensions: inner.extensions, | ||
| timeout: timeout, | 
    
      
    
      Copilot
AI
    
    
    
      Sep 8, 2025 
    
  
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant field initialization. When the field name matches the variable name, you can use the shorthand syntax: timeout instead of timeout: timeout.
| timeout: timeout, | |
| timeout, | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good overall, I think. I haven't had time for a full deep dive, though. I only have some preliminary questions.
|  | ||
| let eval_by_credential = HashMap::new(); | ||
| let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { | ||
| let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the type specifier let hmac_or_prf: GetAssertionHmacOrPrfInput needed? I think it could be removed.
| }, | ||
| ); | ||
| let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { | ||
| let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here and below
| (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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
|  | ||
| use super::{DowngradableRequest, RegisterRequest, UserVerificationRequirement}; | ||
|  | ||
| pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
| use super::{DowngradableRequest, RelyingPartyId, SignRequest, UserVerificationRequirement}; | ||
|  | ||
| #[derive(Debug, Default, Clone, Serialize)] | ||
| pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question here as elsewhere: Should this be redefined here or imported?
No description provided.