packages feed

webauthn 0.9.0.0 → 0.10.0.0

raw patch · 6 files changed

+125/−32 lines, 6 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Crypto.WebAuthn.Operation.Authentication: [aeExpectedOrigin] :: AuthenticationError -> Origin
- Crypto.WebAuthn.Operation.Registration: [reExpectedOrigin] :: RegistrationError -> Origin
+ Crypto.WebAuthn.Operation.Authentication: [aeExpectedOrigins] :: AuthenticationError -> NonEmpty Origin
+ Crypto.WebAuthn.Operation.Registration: [reExpectedOrigins] :: RegistrationError -> NonEmpty Origin
- Crypto.WebAuthn.Operation.Authentication: AuthenticationOriginMismatch :: Origin -> Origin -> AuthenticationError
+ Crypto.WebAuthn.Operation.Authentication: AuthenticationOriginMismatch :: NonEmpty Origin -> Origin -> AuthenticationError
- Crypto.WebAuthn.Operation.Authentication: verifyAuthenticationResponse :: Origin -> RpIdHash -> Maybe UserHandle -> CredentialEntry -> CredentialOptions 'Authentication -> Credential 'Authentication 'True -> Validation (NonEmpty AuthenticationError) AuthenticationResult
+ Crypto.WebAuthn.Operation.Authentication: verifyAuthenticationResponse :: NonEmpty Origin -> RpIdHash -> Maybe UserHandle -> CredentialEntry -> CredentialOptions 'Authentication -> Credential 'Authentication 'True -> Validation (NonEmpty AuthenticationError) AuthenticationResult
- Crypto.WebAuthn.Operation.Registration: RegistrationOriginMismatch :: Origin -> Origin -> RegistrationError
+ Crypto.WebAuthn.Operation.Registration: RegistrationOriginMismatch :: NonEmpty Origin -> Origin -> RegistrationError
- Crypto.WebAuthn.Operation.Registration: verifyRegistrationResponse :: Origin -> RpIdHash -> MetadataServiceRegistry -> DateTime -> CredentialOptions 'Registration -> Credential 'Registration 'True -> Validation (NonEmpty RegistrationError) RegistrationResult
+ Crypto.WebAuthn.Operation.Registration: verifyRegistrationResponse :: NonEmpty Origin -> RpIdHash -> MetadataServiceRegistry -> DateTime -> CredentialOptions 'Registration -> Credential 'Registration 'True -> Validation (NonEmpty RegistrationError) RegistrationResult

Files

changelog.md view
@@ -1,3 +1,13 @@+### 0.10.0.0++* [#184](https://github.com/tweag/webauthn/pull/184) Pass a list of allowed origins instead of a single origin.+  This is a breaking change needed for allowing native apps to use WebAuthn. It is also needed for Relying Parties+  that want to allow multiple subdomains to access WebAuthn credentials.+  Unlike the rest of this library, which strictly follows the L2 version of this spec, this feature is defined+  in the [L3 draft](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin). However because WebAuthn on+  Native Apps is widely deployed through the push of Passkeys we decided to include this feature in this library early.++ ### 0.9.0.0  * [#182](https://github.com/tweag/webauthn/pull/182) Migrate to the crypton library ecosystem.
src/Crypto/WebAuthn/Operation/Authentication.hs view
@@ -36,6 +36,7 @@ import Data.List.NonEmpty (NonEmpty) import Data.Text (Text) import Data.Validation (Validation)+import qualified Data.List.NonEmpty as NE  -- | Errors that may occur during [assertion](https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion) data AuthenticationError@@ -74,11 +75,11 @@         -- | The challenge received from the client, part of the response         aeReceivedChallenge :: M.Challenge       }-  | -- | The origin derived by the client does match the assumed origin+  | -- | The origin derived by the client does match any of the assumed origins     AuthenticationOriginMismatch-      { -- | The origin explicitly passed to the `verifyAuthenticationResponse`+      { -- | The origins explicitly passed to the `verifyAuthenticationResponse`         -- response, set by the RP-        aeExpectedOrigin :: M.Origin,+        aeExpectedOrigins :: NonEmpty M.Origin,         -- | The origin received from the client as part of the client data         aeReceivedOrigin :: M.Origin       }@@ -157,11 +158,32 @@  -- | [(spec)](https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion) -- Verifies a 'M.Credential' response for an [authentication ceremony](https://www.w3.org/TR/webauthn-2/#authentication).+-- -- The 'arSignatureCounterResult' field of the result should be inspected to -- enforce Relying Party policy regarding potentially cloned authenticators.+--+-- Though this library implements the WebAuthn L2 spec, for origin validation we+-- follow the L3 draft. This is because allowing multiple origins is often+-- needed in the wild. See [Validating the origin of a credential](https://www.w3.org/tr/webauthn-3/#sctn-validating-origin) +-- more details.+-- In the simplest case, just a single origin is allowed and this is the 'M.RpId' with @https://@ prepended:+--+-- > verifyAuthenticationResponse (NE.singleton (M.Origin "https://example.org")) ...+--+-- In the more complex case, multiple origins are allowed:+--+-- > verifyAuthenticationResponse (M.Origin <$> "https://example.org" :| ["https://signin.example.org"]) ...+--+-- One might also allow native apps to authenticate:+--+-- > verifyAuthenticationResponse (M.Origin <$> "https://example.org" :| ["ios:bundle-id:org.example.ourapp"]) ...+--+-- See Apple's documentation on [associated domains](https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication/supporting_passkeys/)+-- and Google's documentation on [Digital Asset Links](https://developers.google.com/identity/passkeys/developer-guides) for more information on how to link app+-- origins to your Relying Party ID. verifyAuthenticationResponse ::-  -- | The origin of the server-  M.Origin ->+  -- | The list of allowed origins for the ceremony+  NonEmpty M.Origin ->   -- | The hash of the relying party id   M.RpIdHash ->   -- | The user handle, in case the user is identified already@@ -179,7 +201,7 @@   -- Or in case of success a signature counter result, which should be dealt   -- with   Validation (NonEmpty AuthenticationError) AuthenticationResult-verifyAuthenticationResponse origin rpIdHash midentifiedUser entry options credential = do+verifyAuthenticationResponse origins rpIdHash midentifiedUser entry options credential = do   -- 1. Let options be a new PublicKeyCredentialRequestOptions structure   -- configured to the Relying Party's needs for the ceremony.   -- NOTE: Implemented by caller@@ -290,9 +312,11 @@       AuthenticationChallengeMismatch (M.coaChallenge options) (M.ccdChallenge c)    -- 13. Verify that the value of C.origin matches the Relying Party's origin.-  unless (M.ccdOrigin c == origin) $+  -- NOTE: We follow the L3 draft of the spec here, which allows for multiple origins.+  -- https://www.w3.org/TR/webauthn-3/#rp-op-verifying-assertion-step-origin+  unless (M.ccdOrigin c `elem` NE.toList origins) $     failure $-      AuthenticationOriginMismatch origin (M.ccdOrigin c)+      AuthenticationOriginMismatch origins (M.ccdOrigin c)    -- 14. Verify that the value of C.tokenBinding.status matches the state of   -- Token Binding for the TLS connection over which the attestation was
src/Crypto/WebAuthn/Operation/Registration.hs view
@@ -68,11 +68,11 @@         -- | The challenge received from the client, part of the response         reReceivedChallenge :: M.Challenge       }-  | -- | The returned origin does not match the relying party's origin+  | -- | The returned origin does not match any of the the relying party's origins     RegistrationOriginMismatch-      { -- | The origin explicitly passed to the `verifyRegistrationResponse`+      { -- | The origins explicitly passed to the `verifyRegistrationResponse`         -- response, set by the RP-        reExpectedOrigin :: M.Origin,+        reExpectedOrigins :: NonEmpty M.Origin,         -- | The origin received from the client as part of the client data         reReceivedOrigin :: M.Origin       }@@ -264,13 +264,35 @@ deriving instance ToJSON RegistrationResult  -- | [(spec)](https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential)+-- Verifies a 'M.Credential' response for a [registration ceremony](https://www.w3.org/TR/webauthn-2/#registration-ceremony). +-- -- The resulting 'rrEntry' of this call should be stored in a database by the -- Relying Party. The 'rrAttestationStatement' contains the result of the -- attempted attestation, allowing the Relying Party to reject certain -- authenticators/attempted entry creations based on policy.+--+-- Though this library implements the WebAuthn L2 spec, for origin validation we+-- follow the L3 draft. This is because allowing multiple origins is often+-- needed in the wild. See [Validating the origin of a credential](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) +-- more details.+-- In the simplest case, just a single origin is allowed and this is the 'M.RpId' with @https://@ prepended:+--+-- > verifyRegistrationResponse (NE.singleton (M.Origin "https://example.org")) ...+--+-- In the more complex case, multiple origins are allowed:+--+-- > verifyRegistrationResponse (M.Origin <$> "https://example.org" :| ["https://signin.example.org"]) ...+--+-- One might also allow native apps to authenticate:+--+-- > verifyRegistrationResponse (M.Origin <$> "https://example.org" :| ["ios:bundle-id:org.example.ourapp"]) ...+--+-- See Apple's documentation on [associated domains](https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication/supporting_passkeys/)+-- and Google's documentation on [Digital Asset Links](https://developers.google.com/identity/passkeys/developer-guides) for more information on how to link app+-- origins to your Relying Party ID. verifyRegistrationResponse ::-  -- | The origin of the server-  M.Origin ->+  -- | The list of allowed origins for the ceremony+  NonEmpty M.Origin ->   -- | The relying party id   M.RpIdHash ->   -- | The metadata registry, used for verifying the validity of the@@ -287,7 +309,7 @@   -- Or () in case of a result.   Validation (NonEmpty RegistrationError) RegistrationResult verifyRegistrationResponse-  rpOrigin+  origins   rpIdHash   registry   currentTime@@ -349,9 +371,11 @@           RegistrationChallengeMismatch corChallenge (M.ccdChallenge c)        -- 9. Verify that the value of C.origin matches the Relying Party's origin.-      unless (rpOrigin == M.ccdOrigin c) $+      -- NOTE: We follow the L3 draft of the spec here, which allows for multiple origins.+      -- https://www.w3.org/TR/webauthn-3/#rp-op-registering-a-new-credential-step-origin+      unless (M.ccdOrigin c `elem` NE.toList origins) $         failure $-          RegistrationOriginMismatch rpOrigin (M.ccdOrigin c)+          RegistrationOriginMismatch origins (M.ccdOrigin c)        -- 10. Verify that the value of C.tokenBinding.status matches the state of       -- Token Binding for the TLS connection over which the assertion was
tests/Emulation.hs view
@@ -37,7 +37,7 @@ import Emulation.Client.Arbitrary () import Spec.Util (predeterminedDateTime) import Test.Hspec (SpecWith, describe, it, shouldSatisfy)-import Test.QuickCheck (property)+import Test.QuickCheck (property, (==>))  -- | Custom type to combine the MonadPseudoRandom with the Except monad. We -- force the ChaChaDRG to ensure the App type is completely pure, and@@ -61,12 +61,13 @@ register ::   (Random.MonadRandom m, MonadFail m) =>   AnnotatedOrigin ->+  NE.NonEmpty M.Origin ->   UserAgentConformance ->   Authenticator ->   Meta.MetadataServiceRegistry ->   DateTime ->   m (Either (NE.NonEmpty O.RegistrationError) O.RegistrationResult, Authenticator, M.CredentialOptions 'M.Registration)-register ao conformance authenticator registry now = do+register ao allowedOrigins conformance authenticator registry now = do   -- Generate new random input   assertionChallenge <- M.generateChallenge   userId <- M.generateUserHandle@@ -84,7 +85,7 @@   let registerResult =         toEither $           O.verifyRegistrationResponse-            (aoOrigin ao)+            allowedOrigins             (M.RpIdHash . hash . encodeUtf8 . M.unRpId $ aoRpId ao)             registry             now@@ -95,11 +96,12 @@ login ::   (Random.MonadRandom m, MonadFail m) =>   AnnotatedOrigin ->+  NE.NonEmpty M.Origin ->   UserAgentConformance ->   Authenticator ->   O.CredentialEntry ->   m (Either (NE.NonEmpty O.AuthenticationError) O.SignatureCounterResult)-login ao conformance authenticator ce@O.CredentialEntry {..} = do+login ao allowedOrigins conformance authenticator ce@O.CredentialEntry {..} = do   attestationChallenge <- M.generateChallenge   let options = defaultCog attestationChallenge   -- Perform client assertion emulation with the same authenticator, this@@ -109,7 +111,7 @@     . second O.arSignatureCounterResult     . toEither     $ O.verifyAuthenticationResponse-      (aoOrigin ao)+      allowedOrigins       (M.RpIdHash . hash . encodeUtf8 . M.unRpId $ aoRpId ao)       (Just ceUserHandle)       ce@@ -118,27 +120,60 @@  spec :: SpecWith () spec =-  describe "None" $+  describe "None" $ do+    it "rejects unknown origin during registration" $  do+      property $ \seed authenticator allowedOrigins' origin' -> not (null allowedOrigins') && origin' `notElem` allowedOrigins' ==> do+        let origin = M.Origin origin'+        let allowedOrigins = M.Origin <$> NE.fromList allowedOrigins'+        let annotatedOrigin = AnnotatedOrigin { aoRpId = M.RpId "localhost", aoOrigin = origin }+        let registry = mempty+        let userAgentConformance = mempty+        let Right (registerResult, _, _) = runApp seed (register annotatedOrigin allowedOrigins userAgentConformance authenticator registry predeterminedDateTime)+        registerResult `shouldSatisfy` \case+          Left errors -> any (\case O.RegistrationOriginMismatch _ _ -> True; _ -> False) errors+          Right _ -> False+    it "rejects unknown origin during login" $ do+      property $ \seed authenticator allowedOrigins' origin' -> not (null allowedOrigins') && origin' `notElem` allowedOrigins'  ==> do+        let allowedOrigins = M.Origin <$> NE.fromList allowedOrigins'+        let origin = NE.head allowedOrigins+        let wrongOrigin = M.Origin origin'+        let annotatedOrigin = AnnotatedOrigin { aoRpId = M.RpId "localhost", aoOrigin = origin }+        let wrongAnnotatedOrigin = AnnotatedOrigin { aoRpId = M.RpId "localhost", aoOrigin = wrongOrigin }+        let registry = mempty+        let userAgentConformance = mempty+        let Right (registerResult, authenticator', options) = runApp seed (register annotatedOrigin allowedOrigins userAgentConformance authenticator registry predeterminedDateTime)+        let registerResult' = second O.rrEntry registerResult+        registerResult' `shouldSatisfy` validAttestationResult authenticator userAgentConformance options+        case registerResult' of+          Right credentialEntry -> do+            let Right loginResult = runApp (seed + 1) (login wrongAnnotatedOrigin allowedOrigins userAgentConformance authenticator' credentialEntry)+            loginResult `shouldSatisfy` \case+              Left errors -> any (\case O.AuthenticationOriginMismatch _ _ -> True; _ -> False) errors+              Right _ -> False+          _ -> pure ()+     it "succeeds" $-      property $ \seed authenticator userAgentConformance -> do+      property $ \seed authenticator userAgentConformance  allowedOrigins' -> length allowedOrigins' > 1 ==> do+        let allowedOrigins = M.Origin <$> NE.fromList allowedOrigins'         let annotatedOrigin =               AnnotatedOrigin                 { aoRpId = M.RpId "localhost",-                  aoOrigin = M.Origin "https://localhost:8080"+                  aoOrigin = NE.head allowedOrigins                 }+                 -- Since our emulator only supports None attestation the registry can be left empty.         let registry = mempty         -- We are not currently interested in client or authenticator fails, we         -- only wish to test our relying party implementation and are thus only         -- interested in its errors.-        let Right (registerResult, authenticator', options) = runApp seed (register annotatedOrigin userAgentConformance authenticator registry predeterminedDateTime)+        let Right (registerResult, authenticator', options) = runApp seed (register annotatedOrigin allowedOrigins userAgentConformance authenticator registry predeterminedDateTime)         -- Since we only do None attestation, we only care about the resulting entry         let registerResult' = second O.rrEntry registerResult         registerResult' `shouldSatisfy` validAttestationResult authenticator userAgentConformance options         -- Only if attestation succeeded can we continue with assertion         case registerResult' of           Right credentialEntry -> do-            let Right loginResult = runApp (seed + 1) (login annotatedOrigin userAgentConformance authenticator' credentialEntry)+            let Right loginResult = runApp (seed + 1) (login annotatedOrigin allowedOrigins userAgentConformance authenticator' credentialEntry)             loginResult `shouldSatisfy` validAssertionResult authenticator userAgentConformance           _ -> pure () 
tests/Main.hs view
@@ -80,7 +80,7 @@   let registerResult =         toEither $           O.verifyRegistrationResponse-            origin+            (NE.singleton origin)             (M.RpIdHash . hash . encodeUtf8 . M.unRpId $ rpId)             service             now@@ -125,7 +125,7 @@             registerResult =               toEither $                 O.verifyRegistrationResponse-                  (M.Origin "http://localhost:8080")+                  (NE.singleton $ M.Origin "http://localhost:8080")                   (M.RpIdHash . hash $ ("localhost" :: ByteString.ByteString))                   registry                   predeterminedDateTime@@ -142,7 +142,7 @@             signInResult =               toEither $                 O.verifyAuthenticationResponse-                  (M.Origin "http://localhost:8080")+                  (NE.singleton $ M.Origin "http://localhost:8080")                   (M.RpIdHash . hash $ ("localhost" :: ByteString.ByteString))                   (Just (M.UserHandle "UserId"))                   credentialEntry@@ -163,7 +163,7 @@             registerResult =               toEither $                 O.verifyRegistrationResponse-                  (M.Origin "http://localhost:8080")+                  (NE.singleton $ M.Origin "http://localhost:8080")                   (M.RpIdHash . hash $ ("localhost" :: ByteString.ByteString))                   registry                   predeterminedDateTime@@ -180,7 +180,7 @@             signInResult =               toEither $                 O.verifyAuthenticationResponse-                  (M.Origin "http://localhost:8080")+                  (NE.singleton $ M.Origin "http://localhost:8080")                   (M.RpIdHash . hash $ ("localhost" :: ByteString.ByteString))                   (Just (M.UserHandle "UserId"))                   credentialEntry
webauthn.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: webauthn-version: 0.9.0.0+version: 0.10.0.0 license: Apache-2.0 license-file: LICENSE copyright: