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 +10/−0
- src/Crypto/WebAuthn/Operation/Authentication.hs +32/−8
- src/Crypto/WebAuthn/Operation/Registration.hs +32/−8
- tests/Emulation.hs +45/−10
- tests/Main.hs +5/−5
- webauthn.cabal +1/−1
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: