Skip to content

Provide request deserialization, response serialization #1683

Closed

Description

@dwaite

The addition of ArrayBuffer in the WebAuthn APIs has been an often-cited challenge for relying party developers, as JavaScript does not have integration for converting such buffers to text for serialization via JSON. JSON itself is not necessarily the best format because of the binary content, including for items like extensions.

I'd like to propose deserialization request forms of PublicKeyCredential as long as serialization of the response forms of PublicKeyCredential.

To do this, I would propose a serialization based on a schema (such as CBOR and CDDL) covering the options valid for a PublicKeyCredential. CBOR data is preferred because of the need to preserve the binary signed messages, and due to several internal values being declared as binary identifiers or as CBOR.

Additionally, convenience methods to work in terms of a base64-encoded form of the CBOR data would be desirable for use in text-based API.

As deserialization would result in a PublicKeyCredential object, Javascript would still have full fidelity to inspect and possibly manipulate before making a create() or get() request. Likewise, Javascript would have the ability to inspect the response object before serializing it to share with a remote service.

Deserialization for responses and serialization of requests would both be more difficult due to the possibility of arbitrary manipulation by the user and responses having methods in addition to data.

Activity

  1. MasterKale commented on Nov 15, 2021

    @MasterKale
    Contributor

    Thank you for submitting this ticket, I had intended to file something similar ages ago but never managed to make time for it. In my mind I totally think PublicKeyCredential needs to be expanded in this direction.

    I encourage us to think about how devs work with native JS capabilities and build the spec for them, not build the spec for us and hope JS devs will follow. To get some of my own thoughts out about this:

    • In my mind base64url would be the preferable encoding/decoding scheme because it's specifically called out as a dependency.
    • I think making it easier for developers to consume WebAuthn API via JSON inputs and outputs would go a long way in encouraging adoption without forcing developers to A) figure out WebAuthn serialization for themselves, or B) incorporate another dependency like Github's webauthn-json or my @simplewebauthn/browser.
    • I'd argue JSON over CBOR as the former is the lowest-common denominator data schema, especially in the world of advanced JavaScript projects like Single-Page Applications. JSON can be natively worked with without needing to incorporate any dependencies, and without having numbers handy I'd say almost all front end JS projects make JSON-based REST or GraphQL API calls. This means most JS devs are comfortable thinking about data structures purely as JSON, and have no idea what CBOR-encoding is nor would see the benefits in it when JSON has "worked so well" for them. Not to mention CBOR encoding/decoding would require their own dependencies, so you'd be trading one kind of dependency for another; definitely "one step forward, one step back".

    Whatever direction an effort like this takes, I think the end goal should be to make WebAuthn easier for developers to consume, especially in the browser side of things. If we can accomplish that then adoption will follow. Right now WebAuthn is a PITA to implement in large part because ArrayBuffer's just aren't common in front end JS development. Not to mention there's a lack of native methods to convert to/from Base64URL in the browser which further complicates things when the spec seemingly assumes such "standard library" APIs are available (😂/😭) I think we can tackle this with a smart addition to PublicKeyCredential so long as we keep an eye on how we can make WebAuthn more practical to consume.

  2. dwaite commented on Nov 16, 2021

    @dwaite
    ContributorAuthor

    That would be my preference.

    • I think making it easier for developers to consume WebAuthn API via JSON inputs and outputs would go a long way in encouraging adoption without forcing developers to A) figure out WebAuthn serialization for themselves, or B) incorporate another dependency like Github's webauthn-json or my @simplewebauthn/browser.
    • I'd argue JSON over CBOR as the former is the lowest-common denominator data schema, especially in the world of advanced JavaScript projects like Single-Page Applications. JSON can be natively worked with without needing to incorporate any dependencies, and without having numbers handy I'd say almost all front end JS projects make JSON-based REST or GraphQL API calls. This means most JS devs are comfortable thinking about data structures purely as JSON, and have no idea what CBOR-encoding is nor would see the benefits in it when JSON has "worked so well" for them. Not to mention CBOR encoding/decoding would require their own dependencies, so you'd be trading one kind of dependency for another; definitely "one step forward, one step back".

    We have a lot of values which would be named base64url property values at that point, e.g.

    • numerous identifiers
    • the challenge
    • the client JSON (since it has to be byte-for-byte preserved)
    • the actual signed data
    • all extension requests and responses (which you are pretty much guaranteed to need to handle due to some clients injecting Credential Properties)

    My reasoning for CBOR over JSON:

    Because JSON cannot differentiate text from base64url-encoded binary, the selection of JSON would require a semantic knowledge of the message (including future additions) in order to process. This increases the maintenance of such serialization and deserialization.

    The selection of JSON may also push for duplication of non-encoded forms, for example including what today is getPublicKey() as a duplicated JWK within the serialized JSON response. This particular example duplication of security information and increases the risk of an implementation accidentally relying on data which is not integrity protected.

    I don't feel additional forms of data are necessary. For browser Javascript, we do not need a JSON form to introspect because we have the actual WebAuthn API. For things which a browser needs to understand, we should still focus on raising that information through the API, rather than through serialized forms.

    For the relying party infrastructure processing the data however, we necessitate binary processing already by the nature of the protocols being written in terms of U2F-legacy binary and CBOR binary messages. Semantic transformation to other forms breaks integrity. Base64 encoding binary data does not make the relying party job easier.

    Whatever direction an effort like this takes, I think the end goal should be to make WebAuthn easier for developers to consume, especially in the browser side of things. If we can accomplish that then adoption will follow. Right now WebAuthn is a PITA to implement in large part because ArrayBuffer's just aren't common in front end JS development. Not to mention there's a lack of native methods to convert to/from Base64URL in the browser which further complicates things when the spec seemingly assumes such "standard library" APIs are available (😂/😭) I think we can tackle this with a smart addition to PublicKeyCredential so long as we keep an eye on how we can make WebAuthn more practical to consume.

    Agreed. While a serialized form does not create any mandate, one could see how what is client javascript libraries today might get pared down to either send the serialized form in request/responses directly, or composed within additional API messages which provide something like an authentication workflow.

  3. emlun commented on Nov 16, 2021

    @emlun
    Member

    See also #1619:

    We might consider adding methods similar to getPublicKey() if someone is willing to push for it [...]

    and #1362:

    [...] use of ArrayBuffers is reflecting W3C direction as we understand it and that revisiting that would be too much.

  4. MasterKale commented on Nov 16, 2021

    @MasterKale
    Contributor

    ...if someone is willing to push for it...

    I'll take this on. I'll try and get a draft PR up before too long, I know it'll need a lot of TLC but I think it'd be a worthwhile addition to the spec.

  5. dwaite commented on Nov 17, 2021

    @dwaite
    ContributorAuthor

    A rough first pass at a CDDL (as a potential underpinning of this addition): https://gist.github.com/dwaite/6afb1856e092e02ae6ada85f3ff1655d

    CDDL normally has a single output data item, so there may be multiple descriptions needed if we define specific request/responses for create and get. I'm hardly a CDDL expert, though.

  6. kreichgauer commented on Nov 22, 2021

    @kreichgauer
    Contributor

    ...if someone is willing to push for it...

    I'll take this on. I'll try and get a draft PR up before too long, I know it'll need a lot of TLC but I think it'd be a worthwhile addition to the spec.

    The WebIDL spec already allows for interfaces to provide a toJSON() method. So I could see us here doing something like adding a toJSON() operation to the AuthenticatorAttestationResponse and AuthenticatorAssertionResponse interfaces. Those methods would return a JSON object for the attribute members of those interfaces, with ArrayBuffer valued attributes converted to base64url encoded strings.

    WebIDL doesn't have similar "fromJSON" provisions AFAIK. But if we wanted to allow instantiation of PublicKeyCredentialCreation/RequestOptions from a server-supplied JSON object, we could e.g. define PublicKeyCredential.publicKeyCredentialCreation{Creation,Request}OptionsFromJson(), with basically the inverse conversion rules of toJSON(). I.e., callers pass a JSON object with the respective members, and the method returns an equivalent dictionary with ArrayBuffer-valued attribute values parsed from their base64url encoding.

  7. dwaite commented on Nov 22, 2021

    @dwaite
    ContributorAuthor

    While toJSON() in WebIDL may be convention, I don't believe we could use the default implementation without changes to WebIDL or ECMAScript.

    In particular, the ask has been for this value to be Base64 (or I presume Base64URL) encoded, but such an encoding would need to be defined upstream by one of these two specifications for us to leverage a Default toJSON() implementation from WebIDL.

    We would probably want guidance from WebIDL that our declaration of a fromJSON() would not conflict with WebIDL in the future.

    This would decrease the duplication of description languages, although WebIDL would expect us to define the dictionary formats of the current interface types.

    Either way (WebIDL or CDDL), we have to resolve issue that CredentialCreationOptions/CredentialRequestOptions can not host static methods like fromJSON() - they are dictionaries, not interfaces.

  8. kreichgauer commented on Nov 22, 2021

    @kreichgauer
    Contributor

    While toJSON() in WebIDL may be convention, I don't believe we could use the default implementation without changes to WebIDL or ECMAScript.

    In particular, the ask has been for this value to be Base64 (or I presume Base64URL) encoded, but such an encoding would need to be defined upstream by one of these two specifications for us to leverage a Default toJSON() implementation from WebIDL.

    I don't think there's a requirement that if you define a toJSON operation, it has to use the default operation. For example: https://w3c.github.io/push-api/#dom-pushsubscription-tojson. We would define our own implementation without upstream changes.

    We would probably want guidance from WebIDL that our declaration of a fromJSON() would not conflict with WebIDL in the future.

    This would decrease the duplication of description languages, although WebIDL would expect us to define the dictionary formats of the current interface types.

    Either way (WebIDL or CDDL), we have to resolve issue that CredentialCreationOptions/CredentialRequestOptions can not host static methods like fromJSON() - they are dictionaries, not interfaces.

    My suggestion is to hang them off of PublicKeyCredential, the same way we do with isUserVerifyingPlatformAuthenticatorAvailable(). That way they also won't be generic fromJSON(), but rather publicKeyCredentialCreationCreationOptionsFromJSON() for example, so there is little risk of them conflicting with anything Web IDL might do in the future IMHO.

  9. 18 remaining items

  10. Firstyear commented on Jan 21, 2022

    @Firstyear
    Contributor

    As I'm reading this, you are expressing an opinion that WASM is a "demo" or a "toy" language, and not something you need to seriously consider in the surface area of webauthn and how it interacts with a browser.

    I'm sorry if my message was interpreted that way. I'm trying to verify any requirements you may have.

    If it helps, WASM instructions or Javascript code consuming authentication assertions within the browser has limited utility. You are making an authentication assertion into a locally held and easily user introspectable box. Whether you are protecting e.g. an IndexedDB database by a locally prompted password or WebAuthn, the data is still a local database, exposed one pane over via developer tools.

    Remote infrastructure, whether running on top of WASM or a virtual or native machine architecture would be normally where you would produce challenges and verify the assertion. The choice of execution architecture here should not have an impact over say, the choice of programming language.

    There have been proposals to add e.g. a way to unlock a symmetric key with WebAuthn, which changes this equation significantly - but I'd argue serializing this symmetric key (or making it exportable in general) is probably a bad idea.

    I think you are over thinking this. Just imagine that instead of JS, replace it with WASM. The architecture is still:

    Server -- http --> browser -> JS/WASM -> navigator.credentials
    

    Just replace the dynamic JS parts with WASM, and you get what what I'm asking.

    I am extremely confused by what you are asking here, because I think you are confused about what I'm requesting.

    To ask tersely - you asked for considerations. I asked for what those considerations would be.

    Okay, to be explicit.

    Today, when you perform a browser fetch request to a webauthn endpoint of a server, you receive a JSON response. Deserialising that JSON into the WASM or JS context, it is not possible to create a Uint8Array. As a result, the structure has an array of bytes, but in a different format.

    This means that any RP today MUST create dynamic components that MUST execute in the browser, that MUST alter the content of any structure which is intended to be passed into navigator.credentials apis. This is commonly achieved by base64url encoding and decoding the fields.

    So there a number of potential ways to tackle this:

    1. Provide a "common" javascript blob/file that can be consumed that does the base64 decode/encode as required.
    2. Convince browsers to implement a platform api which allows a JS/WASM object that does the decode/encode as required.
    3. Alter the webauthn specification such that the navigator.credentials apis accept either base64 OR uint8array versions of the fields (per Base64 of fields to simplify javascript/json behaviour #1619 )

    So let's consider this from the JS/WASM context.

    1. Linking and binding to an external piece of javascript, which likely exists in the npm ecosystem may not be easy. In fact, just providing this common js blob does not even need the webauthn group involved, you could setup a project on github today to do this.
    2. and 3. both require webauthn to step in and convince browsers to extend their navigator.credentials apis, and these apis can already be consumed from JS and WASM equally through platform bindings.

    Both of which would be "breaking" changes to what a browser provides, and are desperately needed both so that RP's no longer need JS/WASM to mangle incoming requests so that nav.cred can understand it, but also because a large number of parameters in Webauthn an unsigned which opens the door to a number of potential security issues which extensions poorly defend from.

    There are five buckets of information, somewhat blended together

    1. The unprotected request from the relying party
    2. Client collected data, such as the origin requesting WebAuthn, and client-specified authenticator extensions like CredProtect
    3. The response to the unprotected request by the authenticator, signed by the previously negotiated public credential (on get) or potentially signed by attestation (on create), containing some of the information from 1 & 2.
    4. Non-integrity-protected client information on the response. This includes getTransport() and getClientExtensionResults()
    5. Helper information and Helper API for extracting some information returned from 3 in a non-integrity-protected manner. This includes the credential [[identifier]], getAttestationObject(), and getPublicKey().

    These pieces represent multiple actors and multiple sets of security considerations/attacker models. That is why I hope the helper information (like getPublicKey and getAuthenticatorData) are not serialized as part of responses, as correct validation becomes that much harder if someone does part of their processing on e.g. the non-integrity-verified copy authenticator data. I'd much rather tooling like Webauthn-RS generate their own Helper API as appropriate from the serialized responses.

    I think this section of the comment doesn't apply to the JS/WASM discussion, but I have discussed before about this issue within this group as this has led to CVE's in the past. If this change was to be accepted in a manner where browsers would need to alter their platform apis for navigator.credential, then it would be viable to actually potentially correct this long standing defect in the webauthn specification such that the entire chain of communication is verified rather than small elements.

  11. MasterKale commented on Jan 24, 2022

    @MasterKale
    Contributor

    WASM is really neither here nor there, I suggest we constrain future discussion to JS because the question of how you invoke JS global APIs from WASM will never be the purview of this working group.

  12. dagnelies commented on Feb 15, 2022

    @dagnelies

    If you make a toJSON(), it would aleviate several of the current pain points to use this API and a great step forward. 👍

    Actually, it is a mystery to me why the API currently returns "things" that cannot even be sent to the server directly, but require "post-processing" to do so.

    And if you provide such a toJSON() method, please decode the whole since it is currently "doubly encoded" to avoid CBOR completely. So that you directly see the content of the attestation objects, have direct access to the public key, signatures and so on ....and not simply a base64 CBOR buffer, which besides of being obfuscated, tranfers the burden of CBOR decoding to the server.

    So basically, what was initially suggested in the post there #1362 😁

  13. dwaite commented on Feb 15, 2022

    @dwaite
    ContributorAuthor

    Actually, it is a mystery to me why the API currently returns "things" that cannot even be sent to the server directly, but require "post-processing" to do so.

    From #1362: " On the call of 2020-01-22 it was decided that the use of ArrayBuffers is reflecting W3C direction as we understand it and that revisiting that would be too much. [...]"

    And if you provide such a toJSON() method, please decode the whole since it is currently "doubly encoded" to get rid of CBOR completely. So that you directly see the content of the attestation objects, have direct access to the public key, signatures and so on [...]

    Getting rid of CBOR is not feasible. The security messages themselves are signed binary in a mix of U2F-inherited format and CBOR extensions/attestations.

    Any translated version of the contents would no longer be integrity protected, and integrity protection is paramount to the server understanding authentication is being performed correctly. Any non-binary, non-CBOR form would be generated by the server after validating the binary responses had proper integrity and were correct answers to the challenges created by the server.

    The getPublicKey() method, added in level 2, is meant to provide the public key on creation with the specified limitations. This includes not requesting attestation on registration, as you will need the non-modified binary message in order to verify said attestation.

  14. dagnelies commented on Feb 15, 2022

    @dagnelies

    Sorry for being dumb, but I still don't get it.

    On the client side, you have to transform it in proper JSON to even send it to the server.
    And in order to get get the public key, you have to decode the CBOR stuff inside.

    So, from the perspective of a web developer, I don't understand why being directly decoded would be a problem.

    Any non-binary, non-CBOR form would be generated by the server after validating the binary responses had proper integrity and were correct answers to the challenges created by the server.

    I haven't gone through the validation/verification part. Please take into consideration that an average developer has no idea about U2F and CBOR. What "we" are familiar with is to have a public key in order to verify a signature of some data. Almost always, these are just base64url encoded values sent around. So, in this spec, it feels highly unusual and strange that this unknown CBOR protocol plays such a crucial role ....actually, even after digging into the specs, I still don't know why the original CBOR encoded form would matter ...however with the spec being 165 pages long, it's of course extremely challenging to assimilate, so I probably missed the important part.

    If you want to make something really great, then make a toJWT() method, to obtain a signed Json Web Token 🎉 😁 ...that would be super easy to transmit over the wire, is familiar to every web developer and verification would be a breeze since it is a well known web standard with a mature ecosystem. In short, it would be great!

  15. dwaite commented on Feb 15, 2022

    @dwaite
    ContributorAuthor

    On the client side, you have to transform it in proper JSON to even send it to the server.
    And in order to get get the public key, you have to decode the CBOR stuff inside.
    So, from the perspective of a web developer, I don't understand why being directly decoded would be a problem.

    Right, but the original statement:

    So that you directly see the content of the attestation objects, have direct access to the public key, signatures and so on ....and not simply a base64 CBOR buffer, which besides of being obfuscated, tranfers the burden of CBOR decoding to the server.

    Implies that a full fledged transforms would be applied by this API, rather than simple base64 encoding and embedding the normal configurations and results into a JSON structure. Specifically, that such transforms would eliminate the server's burden for understanding the CBOR format of attestations and extensions.

    There is not much capability for doing this between the client and server.

    The server can certainly create its own simplified forms for business logic and policy evaluation after verifying the credential result, but the server needs to be able to evaluate based on unmodified data in order to do signature integrity checks.

    Add onto this that translation between CBOR and JSON is limited, as CBOR is a superset of JSON. Translating CBOR to JSON means that only responses that the client fully understands would be available.

  16. lgarron commented on Jun 30, 2022

    @lgarron
    Contributor

    I've implemented @MasterKale's API from #1703 in https://github.com/github/webauthn-json as v2.0.0-alpha3.

    // @github/webauthn-json/browser-ponyfill
    
    function supported(): boolean;
    
    function parseCreationOptionsFromJSON(json: JSON): CredentialCreationOptions;
    function parseRequestOptionsFromJSON(json: JSON): CredentialRequestOptions;
    
    // You can call `.toJSON()` on the result or pass directly to `JSON.stringify()`.
    function create(
      options: CredentialCreationOptions,
    ): Promise<PublicKeyCredential>;
    // You can call `.toJSON()` on the result or pass directly to `JSON.stringify()`.
    function get(options: CredentialRequestOptions): Promise<PublicKeyCredential>;

    This ended up fairly simple thanks to @MasterKale's efforts, but I'd love to see if this also works for people in the wild, in case that can help shake out any issues before browsers implement the API.
    (I was able to use it in the github.com codebase without issue.)

    The ability to pass the object directly to JSON.stringify() feels very neat — something that developers might to "by accident" (instead of thinking about whether to call .toJSON() explicitly) but which is the ergonomic way to do it! I think it would be helpful for guides covering that part of the API to call out those mechanics: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description

  17. added a commit that references this issue on Jul 19, 2022
  18. dwaite commented on Nov 9, 2022

    @dwaite
    ContributorAuthor

    bump; is there a reason for this to be open after the merge of PR #1703 ?

  19. MasterKale commented on Nov 9, 2022

    @MasterKale
    Contributor

    No, we should be able to close this now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions