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

webauthn signatures #1193

Merged
merged 39 commits into from
Sep 21, 2023
Merged

webauthn signatures #1193

merged 39 commits into from
Sep 21, 2023

Conversation

imalsogreg
Copy link
Contributor

@imalsogreg imalsogreg commented Apr 14, 2023

This PR adds WebAuthn as a PPKScheme, allowing WebAuthn signatures to be used for user authentication in pact commands.

It also removes some things that were hard to maintain alongside WebAuthn:

  • Associated types for pub/private key and signature in the Scheme class are removed.
  • SomeKeyPair is removed.
  • All methods in the Scheme class except _valid are removed.
  • The ETH scheme and all its support code were removed.

Removing those things has several consequences:

  • Callsites of the removed Scheme class switch to monomorphic versions, assuming ED25519
  • The format-address repl builtin is removed (it relied on a deleted Scheme method)

The removed methods in Scheme are primarily used in client code, for instance when Pact generates a .yaml command file, or when we generate keys in tests. This helps justify the simplification of Scheme, since we're primarily impacting client code. The server-oriented code paths are relatively unchanged.

Implementation

A signature is valid as a WebAuthn signature if

  • it is formatted as a JSON object with a few required fields: clientDataJSON, authenticatorData, and signature
  • These pieces can combine according the the WebAuthn spec to produce a signature valid for the public key, and
  • The PactHash embedded in the clientDataJSON (where it is named "challenge") is the same as the PactHash of the Command.

Clients may issue transactions with webauthn signatures by stringifying a JSON blob according to the spec. An example of such a blob is included in the unit tests.

base64-bytestring upgrade

The webauthn library requires base64-bytestring > 1.0, and pact requires base64-bytestring < 1.0, so this PR also bumps the base64-bytestring library in pact (which will require a bump in chainweb-node, too).

Bumping base64-bytestring is difficult because the new version produces different error messages than the old version (and in some cases, the new version rejects base64-encoded messages that were accepted by old versions). These error messages end up on chain, so to get hash compatibility for historical blocks, we must emulate the old base64-bytestring's behavior. We encountered this issue only in the decodeBase64 native and the str-to-int native, so the behavior-shimming code is colocated with the native definitions (in Native.hs).

@imalsogreg imalsogreg force-pushed the greg/webauthn-signatures branch from 585efde to 1d2315b Compare April 28, 2023 16:18
@imalsogreg imalsogreg force-pushed the greg/webauthn-signatures branch from 1d2315b to c59307c Compare July 26, 2023 22:10
@imalsogreg imalsogreg changed the base branch from lars/wip to master July 26, 2023 22:11
@imalsogreg imalsogreg force-pushed the greg/webauthn-signatures branch from 9dc8ae0 to 70e8a64 Compare August 17, 2023 21:15
@imalsogreg imalsogreg marked this pull request as ready for review August 23, 2023 19:56
@imalsogreg imalsogreg force-pushed the greg/webauthn-signatures branch 2 times, most recently from 76a069d to ecfeba5 Compare August 28, 2023 23:17

-- With Simplified error messages, map every error to a single string.
Left _ | behavior == Simplified ->
return $ Left "Could not base64-decode string"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird indentations here and below on some things

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better now?

Right bs -> case T.decodeUtf8' bs of
Right t -> return $ tStr t
Left _unicodeError -> evalError' i $ "invalid unicode"
Left base64Error -> evalError' i (pretty (T.pack base64Error))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not aligned with line 1349:

...
Left e -> evalError' si (pretty e)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you mean 1349 calls pretty on a String while 1422 calls pretty on a Text? That does seem like a discrepancy. I'll change both to call pretty on a Text, and recheck replay. Good catch!

Comment on lines +189 to +191
WebAuthnSignature
{ authenticatorData
, signature
, clientDataJSON } <- A.eitherDecode (BSL.fromStrict sigBS)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe

Suggested change
WebAuthnSignature
{ authenticatorData
, signature
, clientDataJSON } <- A.eitherDecode (BSL.fromStrict sigBS)
WebAuthnSignature{..} <- A.eitherDecode (BSL.fromStrict sigBS)

using {-# LANGUAGE RecordWildCards #-}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like NamedFieldPuns because it makes the identifiers being brought into scope explicit. Meaning: if someone reading the code didn't know what authenticationData is, or where it comes from, NamedFieldPuns makes it greppable, while RecordWildCards doesn't.

Copy link
Member

@emilypi emilypi Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, construct/destruct the thing directly since we're not even really punning as much as we are constructing directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like NamedFieldPuns even for vanilla record desructuring too, since it removes the possibility of accidentally naming fields out of order. Meaning, I could write:

  WebAuthnSignature sig authData clientData <- decode sigBS
    -- (Is this what you mean by destruct the thing directly?)
  ...

and it would typecheck but be subtly wrong, because I accidentally swapped the field names around for fields that have the same type.

@@ -2104,14 +2104,6 @@ pact> (expect-that "addition" (> 2) (+ 1 2))
```


### format-address {#format-address}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does dropping this have to do with WebAuthn signatures?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping it was required after removing a method from the Scheme typeclass that format-address required.

We have to remove some methods and associated types from Scheme to accommodate webauthn. It's believed be to be unused or only used very rarely.

@@ -145,7 +145,6 @@ library
Pact.Types.Command
Pact.Types.Continuation
Pact.Types.Crypto
Pact.Types.ECDSA
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we dropping this module because the WebAuthn support supercedes the functionality?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we were modifying the Scheme typeclass to support webauthn, another instance of that typeclass, the ETH scheme (and the Pact.Types.ECDSA module that supported it) came up as a place to eliminate some effectively-dead code.

@@ -296,7 +296,7 @@ loadSigData fp = do
Left e -> Left $ "Error loading SigData file " <> fp <> ": " <> show e
Right sd -> Right sd

addSigToSigData :: SomeKeyPair -> SigData a -> IO (SigData a)
addSigToSigData :: Ed25519KeyPair -> SigData a -> IO (SigData a)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we start integrating more with other changes, would we still want to monomorphize the signature scheme?

Copy link
Contributor Author

@imalsogreg imalsogreg Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some integrations we could make that would introduce new PPK schemes. Depending on the details of how those integrations work, they might not see the changes here in Pact.ApiReq, since Pact.ApiReq is only used client-side, e.g. by users signing requests and making API calls to a pact server. A rule of thumb I used in this PR was to be more cautious changing server-side code, but more liberal with client-side code as needed).

A new integration that merely needs signature verification in pact will not be bothered by the monomorphization happening in client code here. But an integration that also wanted client-side support would indeed be less convenient to write, after we remove SomeKeyPair.

import Control.Exception.Safe
import Control.Lens hiding (parts,Fold,contains)
import Control.Monad
import Control.Monad.IO.Class
import qualified Data.Attoparsec.Text as AP
import Data.Bifunctor (first)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't conflict with the import of first from Control.Arrow just above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import Control.Arrow hiding (app, first)
Added it to the hiding so I could have my bifunctor first.

@@ -28,20 +29,29 @@ import qualified Pact.JSON.Encode as J

--------- PPKSCHEME DATA TYPE ---------

data PPKScheme = ED25519 | ETH
data PPKScheme = ED25519 | WebAuthn
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there are really 4 PRs hiding in this one:

  1. Monomorphizing the signature scheme
  2. Dropping the ETH PPKScheme
  3. Dropping format-address
  4. Adding the WebAuthn PPKScheme

I want to hear from @larskuhtz whether we'd like to keep the ETH scheme, if we're going to be doing bridging with Ethereum...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just 4? Don't forget bumping base64-bytestring! :) That's got to count as 3 all on its own.

Copy link
Contributor

@jwiegley jwiegley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good, just a few comments made!

Copy link
Contributor

@sirlensalot sirlensalot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! Main note is we should reconsider removing ETH, see comment in Pact.Types.Crypto.

EDIT scratch that, Eth ECDSA support for Pact payloads is a dead letter. ECDSA support in the context of ethereum compatibility (Solidity ABI, or EVM support) uses a different payload.

@imalsogreg imalsogreg force-pushed the greg/webauthn-signatures branch from a1bcd02 to 7c8fb49 Compare September 6, 2023 20:53
Comment on lines +210 to +211
-- Extract the original challenge from client data.
ClientDataJSON { challenge } <- A.eitherDecode (BSL.fromStrict clientData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried by the amount of aeson instances here being used to decode things that are on-chain, but I don't suggest changing this at this point.

Comment on lines +194 to +196
publicKey <- first show $
Serialise.deserialiseOrFail @WA.CosePublicKey
(BSL.fromStrict pubBS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we restrict the algorithm here to just a few to avoid algorithm confusion attacks? I think @EnoF wanted the algorithm "ES256", which has COSE identifier -7. https://portswigger.net/web-security/jwt/algorithm-confusion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some code that restricts to -7 or -8. (ECDSA SHA-256, and EdDSA respectively).

I informally tested the restriction by setting the requirement to -1 or -8 temporarily and observing that the happy path signature failed.

I'll hopefully be able to generate a test case that we can check in to the repo soon. But it requires spinning up a server that. implements webauthn to create new sample keys with a different signing algorithm.

Comment on lines +221 to +223
-- This test uses example public keys, clientData and authenticatorData from a
-- real WebAuthn browser session from a test user.
verifyWebAuthnSignature :: Spec
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do restrict the signing algorithm we should have a test for it

Co-authored-by: Lars Kuhtz <lars@kadena.io>
@imalsogreg imalsogreg merged commit 445af04 into master Sep 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants