Skip to content
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

Provide request deserialization, response serialization #1683

Closed
dwaite opened this issue Nov 15, 2021 · 35 comments
Closed

Provide request deserialization, response serialization #1683

dwaite opened this issue Nov 15, 2021 · 35 comments
Assignees
Milestone

Comments

@dwaite
Copy link
Contributor

dwaite commented Nov 15, 2021

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.

@MasterKale
Copy link
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.

@dwaite
Copy link
Contributor Author

dwaite commented Nov 16, 2021

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.

@emlun
Copy link
Member

emlun commented Nov 16, 2021

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.

@MasterKale
Copy link
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.

@dwaite
Copy link
Contributor Author

dwaite commented Nov 17, 2021

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.

@nadalin nadalin modified the milestones: L3-WD-01, Futures (catch-all) Nov 17, 2021
@kreichgauer
Copy link
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.

@dwaite
Copy link
Contributor Author

dwaite commented Nov 22, 2021

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.

@kreichgauer
Copy link
Contributor

kreichgauer commented Nov 22, 2021

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.

@dwaite
Copy link
Contributor Author

dwaite commented Dec 2, 2021

The two leading formats would be:

  1. JSON with base64url-encoded properties for ArrayBuffers
  2. base64url-encoded CBOR

I would make two points in favor of CBOR:

  1. This is meant for the server, and the server already needs to work with WebAuthn binary formats as well as CBOR to handle the assertions, to handle attestations and to handle extensions (such as the mandated credProtect by some clients)
  2. Having it be CBOR frees us from some issues with compatibility, while by comparison we can't determine if a RP server intended for a JSON property to be interpreted as a string or an ArrayBuffer without additional rules.

For the second point, additional ArrayBuffer values represented as base64 encoded properties might not be understood as string vs binary properly if a request is sent to a client which does not support it.

This would likely not be an issue where the fromJSON for options is being interpreted by the platform, as @agl mentioned on call. I cannot speak to how this knowledge might complicate environments where the browser and platform have an API between them.

@kreichgauer
Copy link
Contributor

This is meant for the server, and the server already needs to work with WebAuthn binary formats as well as CBOR to handle the assertions, to handle attestations and to handle extensions (such as the mandated credProtect by some clients)

IIRC, we specifically added getAuthenticatorData() because sites were asking us for ways to use basic WebAuthn (i.e. w/o attestation, extensions, etc) without needing to introduce a CBOR dependency. On the flip side, I suspect the vast majority of sites already understand how to pass JSON between a client and a server. So I think JSON is a more natural fit for the web, even if some RPs also have a CBOR dependency already.

additional ArrayBuffer values represented as base64 encoded properties might not be understood as string vs binary properly if a request is sent to a client which does not support it.

I think the way this would work is that when browsers add a new WebAuthn feature that is accessed via a field in PublicKeyCredentialCreation/RequestOptions, they would also add deserialization support to the fromJSON method() (whether it's ArrayBuffer-valued or not). Any unsupported fields would simply be ignored.

I cannot speak to how this knowledge might complicate environments where the browser and platform have an API between them.

At least in Chrome, we would implement this feature in the browser. There's no need to rely on platform APIs.

@timcappalli
Copy link
Member

timcappalli commented Dec 23, 2021

This may be of interest: WICG/proposals#42

@MasterKale
Copy link
Contributor

MasterKale commented Jan 11, 2022

@nicksteele and I put together a document thinking about this from the WACG side of things and what a dev-friendly API would look like for serializing and deserializing WebAuthn options and responses with zero external dependencies:

https://docs.google.com/document/d/e/2PACX-1vTEyAjhn6a3Rqz2KLKcPg7NwoCGO31Lz7E_2zYt8J6Kzey8UUYycv5iukUos5waD4gsml-aEOEs1it0/pub

Below are our current ideas for additions to PublicKeyCredential that would enable developers to send/receive JSON between the front end and back end, and use Base64URL encoding/decoding for values that are ArrayBuffers as per the spec (and thus not transmissible as JSON):

Note: The code below is only intended to represent potential API design. Outputs are not fully fleshed out and may be missing some values.

Registration

Options

const createOpts = PublicKeyCredential.optionsFromJSON({
 method: 'create',
 options: {
   'challenge': 'N1B3...0Fmw',
   'rp': {
     'name': 'Example RP',
     'id': 'localhost',
   },
   'user': {
     'id': 'internalUserId',
     'name': 'user@localhost',
     'displayName': 'user@localhost',
   },
   'excludeCredentials': [
     {
       'id': 'ASdG...om6A',
       'type': 'public-key',
       'transports': ['internal']
     },
   ],
   // ...
 },
});
const resp = await navigator.credentials.create(createOpts);

Response

const resp = await navigator.credentials.create(createOpts);
const respJSON = PublicKeyCredential.responseToJSON({
 method: 'create',
 response: resp,
});
// {
//   "id": "XU9x...47qQ",
//   "rawId": "XU9x...47qQ",
//   "response": {
//         "attestationObject": "o2Nm...MjeQ",
//         "clientDataJSON": "eyJ0...zZX0"
//   },
//   "type": "public-key",
//   "clientExtensionResults": {},
//   "transports": ["usb"]
// }

Authentication

Options

const getOpts = PublicKeyCredential.optionsFromJSON({
 method: 'get',
 options: {
   'rpId': 'localhost',
   'challenge': 'Ecue...5ZDE',
   'allowCredentials': [
     {
       'id': 'ASdG...om6A',
       'type': 'public-key',
       'transports': ['internal'],
     }
   ],
 },
});
const resp = await navigator.credentials.get(getOpts);

Response

const resp = await navigator.credentials.get(getOpts);
const respJSON = PublicKeyCredential.responseToJSON({
 method: 'get',
 response: resp,
});
// {
//   "id": "XU9x...47qQ",
//   "rawId": "XU9x...47qQ",
//   "response": {
//         "authenticatorData": "SZYN...AACA",
//         "clientDataJSON": "eyJ0...zZX0",
//         "signature": "MEUC...TzT8"
//   },
//   "type": "public-key",
//   "clientExtensionResults": {}
// }

@sbweeden
Copy link
Contributor

From an RP consumability perspective I think this is an excellent idea. Minor nits:

  • In the createOpts example, user.id should be shown as a B64URL input example data rather than 'internalUserId' since it is really bytes (same as challenge).
  • The assertion response can optionally include userHandle (not shown in your example)

Another concern is how extensions which use ArrayBuffer for input need to be documented. For example the credBlob extension input is ArrayBuffer rather than JSON. This means that the optionsFromJSON method would need to be "extension aware", which is new behaviour. Not sure if there are any other extensions with this characteristic.

I expect also the JSON/B64URL encodings will eventually need a schema-like specification.

@dwaite
Copy link
Contributor Author

dwaite commented Jan 11, 2022

Another concern is how extensions which use ArrayBuffer for input need to be documented. For example the credBlob extension input is ArrayBuffer rather than JSON. This means that the optionsFromJSON method would need to be "extension aware", which is new behaviour. Not sure if there are any other extensions with this characteristic.

Extensions are what made me prefer a binary CBOR block vs attempting to structure information as JSON - the binary aspect of CBOR means that the browser has to be aware of the format of all extensions it is willing to support.

That said - I believe browsers are all currently white-listing extensions, which means that they can ignore the formatting/validity of requested extensions they do not understand.

@Firstyear
Copy link
Contributor

This is desperately needed as currently to implement webauthn on the server side, you also need to create a matching client JS library. So long as it's consider how this will work in a WASM context for non-js languages, then this is supported by me.

@kreichgauer
Copy link
Contributor

Thanks for the proposal! I recently toyed with this in Chromium, and I have a couple suggestions.

For the response case, wouldn't it be simpler to define a toJSON() method that can be called on the respective PublicKeyCredential instance that the WebAuthn call returns, rather than have a static PublicKeyCredential method that effectively receives the instance plus 'method' as arguments? IMHO that'd be easier to use and it's what the Web IDL spec suggests here.

(Obviously, that's not possible for the request objects, since they're just dictionaries, rather than interface types.)

For either response examples, the top-level PublicKeyCredential-ish object should include an authenticatorAttachment field, I think?

For the create response example, why does transports appear in the top-level dictionary? I believe that information comes from AuthenticatorAttestationResponse.getTransports(), so shouldn't it appear in the object under the response key?

Also for the create response example, I believe the response key dictionary should include the data from the getAuthenticatorData(), getPublicKey(), and getPublicKeyAlgorithm() helpers. I believe those methods were added specifically to aid RPs that wish not to carry a CBOR parsing dependency, so omitting that data would make the toJSON() helper less useful IMHO.

That said - I believe browsers are all currently white-listing extensions, which means that they can ignore the formatting/validity of requested extensions they do not understand.

I think it's a fair assumption that browsers will not pass authenticator extensions in either direction that they don't understand. It's true for Chromium, and likely the case for the remaining WebAuthn implementers as well.

@MasterKale
Copy link
Contributor

Please review my code snippets above from a high level. I regret now not being exhaustive when I wrote all that out because the conversation thus far has been on how imperfect the examples are. My goal was for us to discuss potential API design and then start getting into the weeds on what values should be represented where...

That said there seems to be enough interest now in an effort to add a dependency-free serialization API to WebAuthn L3. I think those of us in the WACG can take this back now and draft a more comprehensive proposal that will include all of the bits my "sanity check examples" left out.

@MasterKale
Copy link
Contributor

For the response case, wouldn't it be simpler to define a toJSON() method that can be called on the respective PublicKeyCredential instance that the WebAuthn call returns...

@kreichgauer You make a great point here, a .toJSON() method on the PublicKeyCredential value returned from navigator.credentials.create() and navigator.credentials.get() would make it even easier to prepare a credential to send to an RP as JSON:

const resp = await navigator.credentials.create(createOpts);
const respJSON = resp.toJSON();
apiClient.postJSON(url, respJSON);

I think this'd be great for serializing responses. It'd still need to be paired with something like the .optionsFromJSON() static method I outlined above 🤔

For either response examples, the top-level PublicKeyCredential-ish object should include an authenticatorAttachment field, I think?

You're right, as of L3 this'll be the case thanks to #1668.

For the create response example, why does transports appear in the top-level dictionary? I believe that information comes from AuthenticatorAttestationResponse.getTransports(), so shouldn't it appear in the object under the response key?

Also for the create response example, I believe the response key dictionary should include the data from the getAuthenticatorData(), getPublicKey(), and getPublicKeyAlgorithm() helpers. I believe those methods were added specifically to aid RPs that wish not to carry a CBOR parsing dependency, so omitting that data would make the toJSON() helper less useful IMHO.

These are the kinds of good questions I figured we'd get to in a PR after I gauged sufficient (current) interest in the idea of serialization helpers to attempt to make a change to the spec. I'm sure there are a few opinions about where values like transports should go in a serialized representation of the PublicKeyCredentials we get back from .create() and .get() and I believe together we can get to something that makes sense.

@lgarron
Copy link
Contributor

lgarron commented Jan 16, 2022

These are the kinds of good questions I figured we'd get to in a PR after I gauged sufficient (current) interest in the idea of serialization helpers to attempt to make a change to the spec. I'm sure there are a few opinions about where values like transports should go in a serialized representation of the PublicKeyCredentials we get back from .create() and .get() and I believe together we can get to something that makes sense.

For what it's worth, @github/webauthn-json uses a simple JSON-based schema to keep track of the necessary conversions, and you can get the current schema using:

npx @github/webauthn-json schema

As of v0.6.3, the schema is:

{
  "credentialCreationOptions": {
    "publicKey": {
      "required": true,
      "schema": {
        "rp": {
          "required": true,
          "schema": "copy"
        },
        "user": {
          "required": true,
          "schema": {
            "id": {
              "required": true,
              "schema": "convert"
            },
            "name": {
              "required": true,
              "schema": "copy"
            },
            "displayName": {
              "required": true,
              "schema": "copy"
            }
          }
        },
        "challenge": {
          "required": true,
          "schema": "convert"
        },
        "pubKeyCredParams": {
          "required": true,
          "schema": "copy"
        },
        "timeout": {
          "required": false,
          "schema": "copy"
        },
        "excludeCredentials": {
          "required": false,
          "schema": [
            {
              "type": {
                "required": true,
                "schema": "copy"
              },
              "id": {
                "required": true,
                "schema": "convert"
              },
              "transports": {
                "required": false,
                "schema": "copy"
              }
            }
          ]
        },
        "authenticatorSelection": {
          "required": false,
          "schema": "copy"
        },
        "attestation": {
          "required": false,
          "schema": "copy"
        },
        "extensions": {
          "required": false,
          "schema": {
            "appid": {
              "required": false,
              "schema": "copy"
            },
            "appidExclude": {
              "required": false,
              "schema": "copy"
            },
            "credProps": {
              "required": false,
              "schema": "copy"
            }
          }
        }
      }
    },
    "signal": {
      "required": false,
      "schema": "copy"
    }
  },
  "publicKeyCredentialWithAttestation": {
    "type": {
      "required": true,
      "schema": "copy"
    },
    "id": {
      "required": true,
      "schema": "copy"
    },
    "rawId": {
      "required": true,
      "schema": "convert"
    },
    "response": {
      "required": true,
      "schema": {
        "clientDataJSON": {
          "required": true,
          "schema": "convert"
        },
        "attestationObject": {
          "required": true,
          "schema": "convert"
        },
        "transports": {
          "required": true,
          "schema": "copy"
        }
      }
    },
    "clientExtensionResults": {
      "required": true,
      "schema": {
        "appid": {
          "required": false,
          "schema": "copy"
        },
        "appidExclude": {
          "required": false,
          "schema": "copy"
        },
        "credProps": {
          "required": false,
          "schema": "copy"
        }
      }
    }
  },
  "credentialRequestOptions": {
    "mediation": {
      "required": false,
      "schema": "copy"
    },
    "publicKey": {
      "required": true,
      "schema": {
        "challenge": {
          "required": true,
          "schema": "convert"
        },
        "timeout": {
          "required": false,
          "schema": "copy"
        },
        "rpId": {
          "required": false,
          "schema": "copy"
        },
        "allowCredentials": {
          "required": false,
          "schema": [
            {
              "type": {
                "required": true,
                "schema": "copy"
              },
              "id": {
                "required": true,
                "schema": "convert"
              },
              "transports": {
                "required": false,
                "schema": "copy"
              }
            }
          ]
        },
        "userVerification": {
          "required": false,
          "schema": "copy"
        },
        "extensions": {
          "required": false,
          "schema": {
            "appid": {
              "required": false,
              "schema": "copy"
            },
            "appidExclude": {
              "required": false,
              "schema": "copy"
            },
            "credProps": {
              "required": false,
              "schema": "copy"
            }
          }
        }
      }
    },
    "signal": {
      "required": false,
      "schema": "copy"
    }
  },
  "publicKeyCredentialWithAssertion": {
    "type": {
      "required": true,
      "schema": "copy"
    },
    "id": {
      "required": true,
      "schema": "copy"
    },
    "rawId": {
      "required": true,
      "schema": "convert"
    },
    "response": {
      "required": true,
      "schema": {
        "clientDataJSON": {
          "required": true,
          "schema": "convert"
        },
        "authenticatorData": {
          "required": true,
          "schema": "convert"
        },
        "signature": {
          "required": true,
          "schema": "convert"
        },
        "userHandle": {
          "required": true,
          "schema": "convert"
        }
      }
    },
    "clientExtensionResults": {
      "required": true,
      "schema": {
        "appid": {
          "required": false,
          "schema": "copy"
        },
        "appidExclude": {
          "required": false,
          "schema": "copy"
        },
        "credProps": {
          "required": false,
          "schema": "copy"
        }
      }
    }
  },
  "version": "0.6.3"
}

@dwaite
Copy link
Contributor Author

dwaite commented Jan 18, 2022

So long as it's consider how this will work in a WASM context for non-js languages, then this is supported by me.

Architecturally, 90+% of browser-hosted WASM has no need to interact with WebAuthn, as it is the server component of the architecture which is challenging and processing the registrations and assertions.

Outside of ZKP experiments such as Cloudflare's and demo applications, WASM code would mostly act analogous to client-side form validation, processing and rejecting created credentials which did not meet the codified server policy. That said, in many cases you might not even gain significant performance benefits from WASM due to serialization and the lack of WebCrypto in WASM.

That said, a JSON serialization does provide a solid default for a serialization of the data into and out of the WASM context for use cases which may benefit.

@Firstyear
Copy link
Contributor

So long as it's consider how this will work in a WASM context for non-js languages, then this is supported by me.

Architecturally, 90+% of browser-hosted WASM has no need to interact with WebAuthn,

And 99% of all Javascript also has nothing to do with WebAuthn, and yet, here we are talking about how to make it accessible to use Webauthn from JS.

as it is the server component of the architecture which is challenging and processing the registrations and assertions.

I'm sorry, are you confused? The issue in this matter is that the types as defined by Webauthn are not possible to deserialise from JSON directly into JS/WASM in a manner that the platform API's can consume, currently forcing all RP implementors to fiddle with the content of the structures.

So it doesn't matter if it's JS or WASM there is currently no way to take "JSON" and feed that to the navigator.credentials apis without messing with it.

Outside of ZKP experiments such as Cloudflare's and demo applications, WASM code would mostly act analogous to client-side form validation, processing and rejecting created credentials which did not meet the codified server policy. That said, in many cases you might not even gain significant performance benefits from WASM due to serialization and the lack of WebCrypto in WASM.

As the author of Webauthn-RS, who implemented the demo site and examples for all client side browser elements in WASM and NOT Javascript, I'd like to disagree. There is also a huge and growing interest in WASM for client side elements in replacement of JS.

As such, WASM is an important use case to consider.

@dwaite
Copy link
Contributor Author

dwaite commented Jan 19, 2022

As the author of Webauthn-RS, who implemented the demo site and examples for all client side browser elements in WASM and NOT Javascript, I'd like to disagree. There is also a huge and growing interest in WASM for client side elements in replacement of JS.

Can you elaborate on this example of client side usage via WASM which is outside local demonstration purposes?

Also, do you have specifics in mind for ways to make the deserialization/serialization more accessible to WASM outside providing the helpers for a consistent format being already described here, e.g. serialization and deserialization formats for requests and responses?

@Firstyear
Copy link
Contributor

As the author of Webauthn-RS, who implemented the demo site and examples for all client side browser elements in WASM and NOT Javascript, I'd like to disagree. There is also a huge and growing interest in WASM for client side elements in replacement of JS.

Can you elaborate on this example of client side usage via WASM which is outside local demonstration purposes?

What is there that needs elaborating? Instead of using JS, you use WASM. As a result, and changes to make it easier to get from JSON -> navigator.credentials.get/create need to work in a WASM context.

Also, do you have specifics in mind for ways to make the deserialization/serialization more accessible to WASM outside providing the helpers for a consistent format being already described here, e.g. serialization and deserialization formats for requests and responses?

Yes, I have previously described these here #1619

@dwaite
Copy link
Contributor Author

dwaite commented Jan 20, 2022

What is there that needs elaborating? Instead of using JS, you use WASM. As a result, and changes to make it easier to get from JSON -> navigator.credentials.get/create need to work in a WASM context.

What would WASM do with the data in this context, e.g. specifically within a browser under the local user's control, other than create a demo app which provides no access into remote systems? The goal is to provide API useful for production systems, not necessarily demonstrations or experimentation. That said, I'm trying to make sure there are not concrete features which would be missed toward that goal.

An example I proposed here was having logic analogous to local form validation. Examples there would be that the credential returned only contains known extensions, or that a created credential has a properly rooted attestation.

The purpose of proposing serialization and deserialization here is specifically that the browser presentation has so little that it can do with WebAuthn. The challenges and assertions are made between the back-end infrastructure and the authenticator.

Yes, I have previously described these here #1619

From that earlier issue, I surmise your goal is encompassed as the one stated here then; we wish to provide methods to go in between some serialization needed by the server and the requests/responses exposed by the API, in order to eliminate the hard requirement for clients and servers to individually define their own implementation-specific approach for that.

You asked for considerations of how it would work in WASM for non-js languages - is there an example of a serialization of the data which would be sufficient for server usage but which would be insufficient for local WASM processing you had in mind?

My suspicion is that there will be more discussion on whether the response serialization should be of integrity-protected binary data and client supplemental data only. The alternative would be having the duplication we currently expose in the API responses, such as returning an additional copy of the public key on credential creation in a more commonly understood format (conditionally, when no attestation was requested)

@Firstyear
Copy link
Contributor

What is there that needs elaborating? Instead of using JS, you use WASM. As a result, and changes to make it easier to get from JSON -> navigator.credentials.get/create need to work in a WASM context.

What would WASM do with the data in this context, e.g. specifically within a browser under the local user's control, other than create a demo app which provides no access into remote systems? The goal is to provide API useful for production systems, not necessarily demonstrations or experimentation. That said, I'm trying to make sure there are not concrete features which would be missed toward that goal.

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.

You asked for considerations of how it would work in WASM for non-js languages - is there an example of a serialization of the data which would be sufficient for server usage but which would be insufficient for local WASM processing you had in mind?

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

My suspicion is that there will be more discussion on whether the response serialization should be of integrity-protected binary data and client supplemental data only. The alternative would be having the duplication we currently expose in the API responses, such as returning an additional copy of the public key on credential creation in a more commonly understood format (conditionally, when no attestation was requested)

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.

@dwaite
Copy link
Contributor Author

dwaite commented Jan 20, 2022

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 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.

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.

@Firstyear
Copy link
Contributor

Firstyear commented Jan 21, 2022

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.

@MasterKale
Copy link
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.

@dagnelies
Copy link

dagnelies commented Feb 15, 2022

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 😁

@dwaite
Copy link
Contributor Author

dwaite commented Feb 15, 2022

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.

@dagnelies
Copy link

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!

@dwaite
Copy link
Contributor Author

dwaite commented Feb 15, 2022

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.

lgarron added a commit to github/webauthn-json that referenced this issue Jun 29, 2022
Release notes:

- Add a `browser-ponyfill` build to match the new API shape from w3c/webauthn#1683
lgarron added a commit to github/webauthn-json that referenced this issue Jun 29, 2022
Release notes:

- Add a `browser-ponyfill` build to match the new API shape from w3c/webauthn#1683
lgarron added a commit to github/webauthn-json that referenced this issue Jun 29, 2022
Release notes:

- Add a `browser-ponyfill` build to match the new API shape from w3c/webauthn#1683
@lgarron
Copy link
Contributor

lgarron commented Jun 30, 2022

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

lgarron added a commit to github/webauthn-json that referenced this issue Jul 19, 2022
Release notes:

- Add a `browser-ponyfill` build to match the new API shape from w3c/webauthn#1683

We've been using the ponyfill on github.com for over two weeks without issue at this point!
dainnilsson added a commit to Yubico/python-fido2 that referenced this issue Aug 10, 2022
@dwaite
Copy link
Contributor Author

dwaite commented Nov 9, 2022

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

@MasterKale
Copy link
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
Projects
None yet
Development

No branches or pull requests

10 participants