tweag / webauthn

A library for parsing and validating webauthn/fido2 credentials
Apache License 2.0
34 stars 11 forks source link

Make library agnostic to the JavaScript encoding used #29

Closed infinisil closed 2 years ago

infinisil commented 3 years ago

Currently we're relying on the JavaScript library webauthn-json's encoding of JavaScript objects returned by navigator.credentials.create() and navigator.credentials.get(). This happens via the FromJSON instances here: https://github.com/tweag/haskell-fido2/blob/247ffd99b9262f98b48891523f94b19e7c1e9f22/fido/Crypto/Fido2/Protocol.hs#L339-L369

Instead we should have functions like fromWebauthnJson :: Value -> Parser (PublicKeyCredential response) that handle this. This then acts as a clear separation of concern between the JavaScript encoding and the Haskell value.

lykahb commented 3 years ago

Is the idea here that instead of the FromJSON AuthenticatorAttestationResponse instance, the library user would pass a custom parser to Aeson? Something like this parse fromWebauthnJson jsonValue?

infinisil commented 3 years ago

@lykahb I've started with it in #32, though quite unfinished at the moment. But in the end it will end up exposing functions like

encodeWebauthnJsonCreationOptions :: PublicKeyCredentialCreationOptions -> ByteString
decodeWebauthnJsonPublicKey :: ByteString -> Either Error (PublicKeyCredential AuthenticatorAttestationResponse)

And the same for login/assertion. These functions are then the only part of the library that deals with the specifics of the JavaScript encoding used.

As an alternate example, the webauthn library uses a CBOR-based encoding, which could be used with different functions of the same type.

lykahb commented 3 years ago

CBOR-based encoding is a rather bad choice for using on the client. It requires adding an extra library to the client bundle. Also, the payload cannot be easily inspected on the network panel. Going forward with JSON helpers is the right way.

Moving the JSON helpers into a separate module is a nice change. However, I do not understand well the the rest of the encoding/decoding JSON changes. The way I understood it at first was that the instances of FromJSON are removed but the parser and toJSON for the top-level data types would remain, so that they can be user as helpers the default schema.

What is in the bytestring for encodeWebauthnJsonCreationOptions and decodeWebauthnJsonPublicKey? Is this a JSON? It is not clear why it is not a Value. Is it to save a call to encode for the caller? In Yesod the handler code that works with Value is more idiomatic than one that works with raw bytestrings. Would this pair of functions be compatible with webauthn-json?

In this part of the #32 PR it is not clear what "raw JavaScript object" and "Haskell JavaScript object" mean. One of them seems to be aeson Value.

-- - bytes -> raw JavaScript object (specific to server javascript used) -- - raw JavaScript object -> Haskell JavaScript object (generic over server javascript used)

infinisil commented 3 years ago

Ah yes, I should clarify that these functions I mentioned would just be the most abstract way of presenting such an interface. The same type could be used whether it was JSON or anything else. The ByteString would represent what is being sent/received in the HTTP request/response's body.

However as you also mentioned, I noticed quickly that this won't work particularly well for web frameworks which generally expect JSON. So instead it should end up look something like this:

newtype WebauthnJsonCreationOptions = WebauthnJsonCreationOptions PublicKeyCredentialCreationOptions

instance ToJSON WebauthnJsonCreationOptions

newtype WebauthnJsonPublicKeyAttestation = WebauthnJsonPublicKeyAttestation (PublicKeyCredential AuthenticatorAttestationResponse)

instance FromJSON WebauthnJsonPublicKeyAttestation

This should become clearer as I continue working on this. The JSON decoding separation will be pretty nice, as it's been one of the most confusing bits.

arianvp commented 2 years ago

Indeed a good idea to decouple this. One could send the data in any form. JSON, ProtoBuf, or even form-url-encoded with a traditional <input type=hidden> form...

infinisil commented 2 years ago

This is essentially done with https://github.com/tweag/haskell-fido2/pull/32 and https://github.com/tweag/haskell-fido2/pull/37.

See https://github.com/tweag/haskell-fido2/blob/master/fido/Crypto/Fido2/Model/JavaScript.hs, which contains datatypes that represent how the JavaScript side encodes them. These types implement FromJSON and ToJSON for the JSON format used by webauthn-json, but other decoders/encoders can also be written relatively easily and independently of this library.

That JavaScript representation is then decoded into a more well-typed Haskell model declared in https://github.com/tweag/haskell-fido2/blob/master/fido/Crypto/Fido2/Model.hs. The decoding/encoding happens in https://github.com/tweag/haskell-fido2/blob/master/fido/Crypto/Fido2/Model/JavaScript/Decoding.hs and https://github.com/tweag/haskell-fido2/blob/master/fido/Crypto/Fido2/Model/JavaScript/Encoding.hs respectively.