ucam-webauth (empty) → 0.1.0.0
raw patch · 10 files changed
+1206/−0 lines, 10 filesdep +QuickCheckdep +aesondep +attoparsec
Dependencies added: QuickCheck, aeson, attoparsec, base, bytestring, containers, cryptonite, errors, generic-random, here, hspec, http-api-data, http-types, microlens, microlens-mtl, mtl, parser-combinators, pem, quickcheck-instances, text, time, time-qq, ucam-webauth-types, x509
Files
- Changelog.md +14/−0
- LICENSE +203/−0
- README.md +56/−0
- src/UcamWebauth.hs +310/−0
- src/UcamWebauth/Internal.hs +23/−0
- src/UcamWebauth/Parser.hs +218/−0
- src/UcamWebauth/WLS.hs +89/−0
- test/Spec.hs +1/−0
- test/UcamWebauthSpec.hs +182/−0
- ucam-webauth.cabal +110/−0
+ Changelog.md view
@@ -0,0 +1,14 @@+# Changelog++This changelog is shared among all the packages within the same project.++## 0.1.0.0 - 2018-12-09++Initial versions of `ucam-webauth` and `ucam-webauth-types`.++---++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/).
+ LICENSE view
@@ -0,0 +1,203 @@++ Apache License+ Version 2.0, January 2004+ http://www.apache.org/licenses/++ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++ 1. Definitions.++ "License" shall mean the terms and conditions for use, reproduction,+ and distribution as defined by Sections 1 through 9 of this document.++ "Licensor" shall mean the copyright owner or entity authorized by+ the copyright owner that is granting the License.++ "Legal Entity" shall mean the union of the acting entity and all+ other entities that control, are controlled by, or are under common+ control with that entity. For the purposes of this definition,+ "control" means (i) the power, direct or indirect, to cause the+ direction or management of such entity, whether by contract or+ otherwise, or (ii) ownership of fifty percent (50%) or more of the+ outstanding shares, or (iii) beneficial ownership of such entity.++ "You" (or "Your") shall mean an individual or Legal Entity+ exercising permissions granted by this License.++ "Source" form shall mean the preferred form for making modifications,+ including but not limited to software source code, documentation+ source, and configuration files.++ "Object" form shall mean any form resulting from mechanical+ transformation or translation of a Source form, including but+ not limited to compiled object code, generated documentation,+ and conversions to other media types.++ "Work" shall mean the work of authorship, whether in Source or+ Object form, made available under the License, as indicated by a+ copyright notice that is included in or attached to the work+ (an example is provided in the Appendix below).++ "Derivative Works" shall mean any work, whether in Source or Object+ form, that is based on (or derived from) the Work and for which the+ editorial revisions, annotations, elaborations, or other modifications+ represent, as a whole, an original work of authorship. For the purposes+ of this License, Derivative Works shall not include works that remain+ separable from, or merely link (or bind by name) to the interfaces of,+ the Work and Derivative Works thereof.++ "Contribution" shall mean any work of authorship, including+ the original version of the Work and any modifications or additions+ to that Work or Derivative Works thereof, that is intentionally+ submitted to Licensor for inclusion in the Work by the copyright owner+ or by an individual or Legal Entity authorized to submit on behalf of+ the copyright owner. For the purposes of this definition, "submitted"+ means any form of electronic, verbal, or written communication sent+ to the Licensor or its representatives, including but not limited to+ communication on electronic mailing lists, source code control systems,+ and issue tracking systems that are managed by, or on behalf of, the+ Licensor for the purpose of discussing and improving the Work, but+ excluding communication that is conspicuously marked or otherwise+ designated in writing by the copyright owner as "Not a Contribution."++ "Contributor" shall mean Licensor and any individual or Legal Entity+ on behalf of whom a Contribution has been received by Licensor and+ subsequently incorporated within the Work.++ 2. Grant of Copyright License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ copyright license to reproduce, prepare Derivative Works of,+ publicly display, publicly perform, sublicense, and distribute the+ Work and such Derivative Works in Source or Object form.++ 3. Grant of Patent License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ (except as stated in this section) patent license to make, have made,+ use, offer to sell, sell, import, and otherwise transfer the Work,+ where such license applies only to those patent claims licensable+ by such Contributor that are necessarily infringed by their+ Contribution(s) alone or by combination of their Contribution(s)+ with the Work to which such Contribution(s) was submitted. If You+ institute patent litigation against any entity (including a+ cross-claim or counterclaim in a lawsuit) alleging that the Work+ or a Contribution incorporated within the Work constitutes direct+ or contributory patent infringement, then any patent licenses+ granted to You under this License for that Work shall terminate+ as of the date such litigation is filed.++ 4. Redistribution. You may reproduce and distribute copies of the+ Work or Derivative Works thereof in any medium, with or without+ modifications, and in Source or Object form, provided that You+ meet the following conditions:++ (a) You must give any other recipients of the Work or+ Derivative Works a copy of this License; and++ (b) You must cause any modified files to carry prominent notices+ stating that You changed the files; and++ (c) You must retain, in the Source form of any Derivative Works+ that You distribute, all copyright, patent, trademark, and+ attribution notices from the Source form of the Work,+ excluding those notices that do not pertain to any part of+ the Derivative Works; and++ (d) If the Work includes a "NOTICE" text file as part of its+ distribution, then any Derivative Works that You distribute must+ include a readable copy of the attribution notices contained+ within such NOTICE file, excluding those notices that do not+ pertain to any part of the Derivative Works, in at least one+ of the following places: within a NOTICE text file distributed+ as part of the Derivative Works; within the Source form or+ documentation, if provided along with the Derivative Works; or,+ within a display generated by the Derivative Works, if and+ wherever such third-party notices normally appear. The contents+ of the NOTICE file are for informational purposes only and+ do not modify the License. You may add Your own attribution+ notices within Derivative Works that You distribute, alongside+ or as an addendum to the NOTICE text from the Work, provided+ that such additional attribution notices cannot be construed+ as modifying the License.++ You may add Your own copyright statement to Your modifications and+ may provide additional or different license terms and conditions+ for use, reproduction, or distribution of Your modifications, or+ for any such Derivative Works as a whole, provided Your use,+ reproduction, and distribution of the Work otherwise complies with+ the conditions stated in this License.++ 5. Submission of Contributions. Unless You explicitly state otherwise,+ any Contribution intentionally submitted for inclusion in the Work+ by You to the Licensor shall be under the terms and conditions of+ this License, without any additional terms or conditions.+ Notwithstanding the above, nothing herein shall supersede or modify+ the terms of any separate license agreement you may have executed+ with Licensor regarding such Contributions.++ 6. Trademarks. This License does not grant permission to use the trade+ names, trademarks, service marks, or product names of the Licensor,+ except as required for reasonable and customary use in describing the+ origin of the Work and reproducing the content of the NOTICE file.++ 7. Disclaimer of Warranty. Unless required by applicable law or+ agreed to in writing, Licensor provides the Work (and each+ Contributor provides its Contributions) on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+ implied, including, without limitation, any warranties or conditions+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+ PARTICULAR PURPOSE. You are solely responsible for determining the+ appropriateness of using or redistributing the Work and assume any+ risks associated with Your exercise of permissions under this License.++ 8. Limitation of Liability. In no event and under no legal theory,+ whether in tort (including negligence), contract, or otherwise,+ unless required by applicable law (such as deliberate and grossly+ negligent acts) or agreed to in writing, shall any Contributor be+ liable to You for damages, including any direct, indirect, special,+ incidental, or consequential damages of any character arising as a+ result of this License or out of the use or inability to use the+ Work (including but not limited to damages for loss of goodwill,+ work stoppage, computer failure or malfunction, or any and all+ other commercial damages or losses), even if such Contributor+ has been advised of the possibility of such damages.++ 9. Accepting Warranty or Additional Liability. While redistributing+ the Work or Derivative Works thereof, You may choose to offer,+ and charge a fee for, acceptance of support, warranty, indemnity,+ or other liability obligations and/or rights consistent with this+ License. However, in accepting such obligations, You may act only+ on Your own behalf and on Your sole responsibility, not on behalf+ of any other Contributor, and only if You agree to indemnify,+ defend, and hold each Contributor harmless for any liability+ incurred by, or claims asserted against, such Contributor by reason+ of your accepting any such warranty or additional liability.++ END OF TERMS AND CONDITIONS++ APPENDIX: How to apply the Apache License to your work.++ To apply the Apache License to your work, attach the following+ boilerplate notice, with the fields enclosed by brackets "{}"+ replaced with your own identifying information. (Don't include+ the brackets!) The text should be enclosed in the appropriate+ comment syntax for the file format. We also recommend that a+ file or class name and description of purpose be included on the+ same "printed page" as the copyright notice for easier+ identification within third-party archives.++ Copyright {yyyy} {name of copyright owner}++ Licensed under the Apache License, Version 2.0 (the "License");+ you may not use this file except in compliance with the License.+ You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++ Unless required by applicable law or agreed to in writing, software+ distributed under the License is distributed on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ See the License for the specific language governing permissions and+ limitations under the License.+
+ README.md view
@@ -0,0 +1,56 @@+---+title: Ucam Webauth +author: David Baynard +date: 09 Dec 2018 +...++[](https://travis-ci.com/dbaynard/UcamWebauth)++# <https://raven.cam.ac.uk/project/>++The University of Cambridge _Raven_ service uses the _Ucam Webauth_ protocol.++This repository contains a number of Haskell libraries to interact with this system.++# ucam-webauth++[](https://hackage.haskell.org/package/ucam-webauth)+[](http://stackage.org/lts-13/package/ucam-webauth)+[](http://stackage.org/nightly/package/ucam-webauth)++This implements the client authentication protocol; specifically, the validation.++# ucam-webauth-types++[](https://hackage.haskell.org/package/ucam-webauth-types)+[](http://stackage.org/lts-13/package/ucam-webauth-types)+[](http://stackage.org/nightly/package/ucam-webauth-types)++This implements data types for the client authentication protocol.++There is an internal package which is *not* recommended for use.+Its only purpose is to split the core functionality among packages for minimal ghcjs dependencies.++# raven-wai++[](https://hackage.haskell.org/package/raven-wai)+[](http://stackage.org/lts-13/package/raven-wai)+[](http://stackage.org/nightly/package/raven-wai)++This adds [wai](//hackage.haskell.org/package/wai) middleware enabling authentication using _Raven_.++# servant-raven++[](https://hackage.haskell.org/package/servant-raven)+[](http://stackage.org/lts-13/package/servant-raven)+[](http://stackage.org/nightly/package/servant-raven)++API combinators for [servant](//hackage.haskell.org/package/servant), using [servant-auth](//hackage.haskell.org/package/servant-auth).++# servant-raven-server++[](https://hackage.haskell.org/package/servant-raven-server)+[](http://stackage.org/lts-13/package/servant-raven-server)+[](http://stackage.org/nightly/package/servant-raven-server)++The handlers for [servant](//hackage.haskell.org/package/servant).
+ src/UcamWebauth.hs view
@@ -0,0 +1,310 @@+{-|+Module : UcamWebauth+Description : The Ucam-Webauth protocol, from the University of Cambridge+Maintainer : David Baynard <ucamwebauth@baynard.me>++Key parts of the implementation of the protocol itself.++<https://raven.cam.ac.uk/project/waa2wls-protocol.txt>++-}++{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE+ PackageImports+ , DataKinds+ , FlexibleContexts+ , FlexibleInstances+ , OverloadedStrings+ , RecordWildCards+ , ScopedTypeVariables+ , TupleSections+ , ViewPatterns+ #-}++module UcamWebauth+ ( module X++ -- Parser+ , ucamResponseParser++ , authInfo+ , maybeAuthInfo+ , authCode++ , validateAuthResponse+ ) where++import "base" Control.Applicative+import "errors" Control.Error hiding (catMaybes)+import "base" Control.Monad+import "mtl" Control.Monad.Except+import "cryptonite" Crypto.Hash.Algorithms+import "cryptonite" Crypto.PubKey.RSA.PKCS15+import "cryptonite" Crypto.PubKey.RSA.Types+import "aeson" Data.Aeson.Types (FromJSON, ToJSON)+import "attoparsec" Data.Attoparsec.ByteString.Char8 hiding (count, take)+import "base" Data.Bifunctor+import "bytestring" Data.ByteString (ByteString)+import qualified "bytestring" Data.ByteString.Char8 as B+import "base" Data.Coerce+import "base" Data.List.NonEmpty (NonEmpty)+import "containers" Data.Map.Strict (Map)+import qualified "containers" Data.Map.Strict as MapS+import "pem" Data.PEM+import "text" Data.Text (Text)+import qualified "text" Data.Text as T+import "text" Data.Text.Encoding hiding (decodeASCII)+import "time" Data.Time (diffUTCTime, getCurrentTime)+import "x509" Data.X509+import "microlens" Lens.Micro+import "microlens-mtl" Lens.Micro.Mtl+import "base" System.IO (withFile, IOMode(..))+import "ucam-webauth-types" UcamWebauth.Data as X+import "ucam-webauth-types" UcamWebauth.Data.Internal+import "this" UcamWebauth.Internal+import "this" UcamWebauth.Parser+import "this" UcamWebauth.WLS+import "http-api-data" Web.HttpApiData++------------------------------------------------------------------------------+-- * Top level functions++{-|+ If the supplied response is valid and corresponds to the settigns, return a 'UcamWebauthInfo' value.++-}+authInfo+ :: (MonadIO m, MonadPlus m, MonadError Text m)+ => SetWAA a+ -> MaybeValidResponse a+ -> m (UcamWebauthInfo a)+authInfo waa = getAuthInfo <=< validateAuthResponse waa++{-|+ 'maybeAuthInfo' takes the 'AuthRequest' from its environment, and a 'ByteString' containing the @WLS@+ response, and if the response is valid, returns a 'UcamWebauthInfo' value.++ TODO When the errors returned can be usefully used, ensure this correctly returns a lifted+ 'Either b (UcamWebauthInfo a)' response.+-}+maybeAuthInfo+ :: (FromJSON a, MonadIO m, MonadPlus m, MonadError Text m)+ => SetWAA a+ -> ByteString+ -> m (UcamWebauthInfo a)+maybeAuthInfo waa = getAuthInfo <=< maybeAuthCode waa++{-|+ A helper function to parse and validate a response from a @WLS@.+-}+maybeAuthCode+ :: (FromJSON a, MonadIO m, MonadPlus m, MonadError Text m)+ => SetWAA a+ -> ByteString+ -> m (ValidResponse a)+maybeAuthCode waa = validateAuthResponse waa <=< authCode++{-|+ Parse the response from a @WLS@.+-}+authCode+ :: (FromJSON a, MonadPlus m)+ => ByteString+ -> m (MaybeValidResponse a)+authCode = liftMaybe . maybeResult . flip feed "" . parse ucamResponseParser++-- | Parse a not-yet-validated 'SignedAuthResponse' from a form response.+--+-- The orphan instance is necessary as this requires the parser defined in+-- this package.+instance FromJSON a => FromHttpApiData (MaybeValidResponse a) where+ parseQueryParam = first T.pack . parseOnly ucamResponseParser . encodeUtf8++instance ToJSON a => ToHttpApiData (MaybeValidResponse a) where+ toUrlPiece = wlsEncodeSign++instance FromJSON a => FromHttpApiData (AuthResponse a) where+ parseQueryParam = first T.pack . parseOnly ucamAuthResponseParser . encodeUtf8++instance ToJSON a => ToHttpApiData (AuthResponse a) where+ toUrlPiece = wlsEncode++------------------------------------------------------------------------------+-- * Validation++guardE+ :: forall e m . (MonadError e m, Alternative m)+ => e -> Bool -> m ()+guardE e boolean = guard boolean <|> throwError e++{-|+ Validate the Authentication Response++ 1. Validate the key id+ 2. Validate the cryptographic signature against the relevant key+ 3. Validate the issue time+ 4. Validate the url is the same as that transmitted+ 5. Validate the auth and sso values are valid++ The protocol requires a valid input, but 'ucamResponseParser' holds all the relevant logic+ as the only way to produce a 'SignedAuthResponse' is with the parser.++ This is the only way to produce a 'Valid' 'SignedAuthResponse', and therefore an 'AuthInfo'.+-}+validateAuthResponse+ :: forall a m . (MonadIO m, MonadPlus m, MonadError Text m)+ => SetWAA a+ -> MaybeValidResponse a+ -> m (ValidResponse a)+validateAuthResponse waa sar = do+ guardE "Key invalid" .+ validateKid waa =<< liftMaybe (sar ^. ucamAKid)+ guardE "Signature invalid" <=<+ validateSig waa $ sar+ guardE "Issue time invalid" <=<+ validateIssueTime waa $ sar ^. ucamAResponse+ guardE "Url does not match transmittion" .+ validateUrl waa $ sar ^. ucamAResponse+ guardE "Authentication type invalid" <=<+ validateAuthTypes waa $ sar ^. ucamAResponse+ return . makeValid $ sar+ where+ makeValid :: MaybeValidResponse a -> ValidResponse a+ makeValid = coerce++------------------------------------------------------------------------------+-- ** Key ID++{-|+ Check the kid is valid+-}+validateKid :: SetWAA a -> KeyID -> Bool+validateKid = flip elem . view (wSet . validKids) . configWAA++------------------------------------------------------------------------------+-- ** Signature++{-|+ Validate the signature, getting the key using 'readRSAKeyFile'+-}+validateSig+ :: (MonadPlus m, MonadIO m)+ => SetWAA a+ -> MaybeValidResponse a+ -> m Bool+validateSig (configWAA -> waa) = validateSigKey (readRSAKeyFile $ waa ^. wSet . importedKeys)++decodeRSAPubKey+ :: ByteString -- ^ The data representing a public key as PEM.+ -> Maybe PublicKey -- ^ @'Just' 'PublicKey'@ if RSA, 'Nothing' otherwise.+decodeRSAPubKey = hush . f++ where+ f :: ByteString -> Either String PublicKey+ f = getRSAKey . certPubKey . getCertificate+ <=< decodeSignedCertificate . pemContent+ <=< headErr "Empty list"+ <=< pemParseBS++ getRSAKey :: Alternative f => PubKey -> f PublicKey+ getRSAKey (PubKeyRSA x) = pure x+ getRSAKey _ = empty++{-|+ This assumes keys are PEM self-signed certificates in the ‘static’ directory, named++ @pubkey/key/.crt@++ where @/key/@ should be replaced by the 'KeyID' /e.g./ @pubkey2.crt@+-}+readRSAKeyFile+ :: (MonadIO m, Alternative m)+ => Map KeyID ByteString+ -> KeyID+ -> m PublicKey+readRSAKeyFile keymap key = case key `MapS.lookup` keymap of+ Just bs -> liftMaybe . decodeRSAPubKey $ bs+ Nothing -> let file = "static/pubkey" <> (B.unpack . unKeyID) key <> ".crt" in+ liftMaybe <=< liftIO . withFile file ReadMode+ $ pure . decodeRSAPubKey <=< B.hGetContents++validateSigKey+ :: MonadPlus m+ => (KeyID+ -> m PublicKey) -- ^ Get an RSA 'PublicKey' from somewhere, with the possibility of failing.+ -> MaybeValidResponse a+ -> m Bool -- ^ 'True' for a verified signature, 'False' for a verified invalid signature, and 'mzero' for an inability to validate+validateSigKey importKey sar =+ pure . rsaValidate <=< importKey <=< liftMaybe $ sar ^. ucamAKid+ where+ rsaValidate :: PublicKey -> Bool+ rsaValidate key = verify (Just SHA1) key message signature+ message :: ByteString+ message = sar ^. ucamAToSign+ signature :: ByteString+ signature = maybe mempty decodeUcamB64 . view ucamASig $ sar++------------------------------------------------------------------------------+-- ** Issue time++{-|+ Validate the time of issue is within 'syncTimeOut' of the current time.++ TODO Uses 'getCurrentTime'. There may be a better implementation.+-}+validateIssueTime+ :: (MonadIO m)+ => SetWAA a+ -> AuthResponse a+ -> m Bool+validateIssueTime (configWAA -> waa) ar =+ (waa ^. wSet . syncTimeOut >) . flip diffUTCTime (ar ^. ucamAIssue)+ <$> liftIO getCurrentTime++------------------------------------------------------------------------------+-- ** Url++{-|+ Check the url parameter matches that sent in the 'AuthRequest'+-}+validateUrl+ :: SetWAA a+ -> AuthResponse a+ -> Bool+validateUrl (configWAA -> waa) =+ (== waa ^. aReq . ucamQUrl) . view ucamAUrl++------------------------------------------------------------------------------+-- ** Authentication type++{-|+ Check the authentication type matches that sent.++ * If the iact variable is Yes, only return 'True' if the aauth value is acceptable.+ * If the iact variable is No, only return 'True' if sso contains a value that is acceptable.+ * If the iact variable is unset, return 'True' if there is an acceptable value in either field.+-}+validateAuthTypes+ :: forall a f . (Alternative f)+ => SetWAA a+ -> AuthResponse a+ -> f Bool+validateAuthTypes (configWAA -> waa) ar =+ maybe validateAnyAuth validateSpecificAuth $ waa ^. wSet . needReauthentication++ where+ isAcceptableAuth :: AuthType -> Bool+ isAcceptableAuth = flip elem $ waa ^. wSet . authAccepted++ anyAuth :: Maybe AuthType -> Maybe (NonEmpty AuthType) -> Bool+ anyAuth Nothing (Just x) = any isAcceptableAuth x+ anyAuth (Just x) Nothing = isAcceptableAuth x+ anyAuth _ _ = False++ validateAnyAuth :: f Bool+ validateAnyAuth = pure $ (ar ^. ucamAAuth) `anyAuth` (ar ^. ucamASso)++ validateSpecificAuth :: YesNo -> f Bool+ validateSpecificAuth Yes = isAcceptableAuth <$> liftMaybe (ar ^. ucamAAuth)+ validateSpecificAuth _ = any isAcceptableAuth <$> liftMaybe (ar ^. ucamASso)
+ src/UcamWebauth/Internal.hs view
@@ -0,0 +1,23 @@+{-# OPTIONS_HADDOCK hide, not_here #-}+{-# LANGUAGE+ PackageImports+ #-}++{-|+Module : UcamWebauth.Internal+Description : Ucam-Webauth protocol internals+Maintainer : David Baynard <ucamwebauth@baynard.me>++-}++module UcamWebauth.Internal+ ( module UcamWebauth.Internal+ ) where++import "base" Control.Applicative (empty, Alternative)++{-|+ Lift a 'Maybe' value.+-}+liftMaybe :: Alternative f => Maybe a -> f a+liftMaybe = maybe empty pure
+ src/UcamWebauth/Parser.hs view
@@ -0,0 +1,218 @@+{-# OPTIONS_HADDOCK hide, not_here #-}+{-# LANGUAGE+ PackageImports+ , ApplicativeDo+ , DataKinds+ , OverloadedStrings+ , RecordWildCards+ , ScopedTypeVariables+ , ViewPatterns+ #-}++{-|+Module : UcamWebauth.Parser+Description : Parsers for the UCam-Webauth protocol+Maintainer : David Baynard <ucamwebauth@baynard.me>++-}++module UcamWebauth.Parser+ ( ucamResponseParser+ , ucamAuthResponseParser+ ) where++import "base" Control.Applicative+import "parser-combinators" Control.Applicative.Combinators.NonEmpty+import "base" Control.Arrow ((***))+import "base" Control.Monad ((<=<))+import "aeson" Data.Aeson (FromJSON)+import qualified "aeson" Data.Aeson as A+import qualified "attoparsec" Data.Attoparsec.ByteString.Char8 as A+import "attoparsec" Data.Attoparsec.ByteString.Char8 hiding (count, take, sepBy1)+import "attoparsec" Data.Attoparsec.Combinator (lookAhead)+import "bytestring" Data.ByteString (ByteString)+import "ucam-webauth-types" Data.ByteString.B64+import "bytestring" Data.ByteString.Builder (stringUtf8, toLazyByteString)+import qualified "bytestring" Data.ByteString.Char8 as B8+import qualified "bytestring" Data.ByteString.Lazy as BSL+import "base" Data.Char (isAlphaNum)+import "base" Data.List.NonEmpty (NonEmpty)+import "base" Data.Maybe+import "text" Data.Text (Text)+import "text" Data.Text.Encoding+import "time" Data.Time (UTCTime)+import "time" Data.Time.LocalTime+import "http-types" Network.HTTP.Types+import "ucam-webauth-types" UcamWebauth.Data+import "ucam-webauth-types" UcamWebauth.Data.Internal++------------------------------------------------------------------------------+-- * 'Parser's++{-|+ Parse the response from the @WLS@++ As a reminder, the 'MaybeValid' symbol indicates the response has not yet been verified.+-}+ucamResponseParser :: FromJSON a => Parser (MaybeValidResponse a)+ucamResponseParser = do+ (_ucamAToSign, _ucamAResponse@AuthResponse{..}) <- noBang . match $ ucamAuthResponseParser+ (_ucamAKid, _ucamASig) <- parseKidSig _ucamAStatus <?> "A Kid signature"+ _ <- endOfInput+ pure SignedAuthResponse{..}++ucamAuthResponseParser :: FromJSON a => Parser (AuthResponse a)+ucamAuthResponseParser = do+ _ucamAVer <- noBang wlsVersionParser+ _ucamAStatus <- noBang responseCodeParser+ _ucamAMsg <- maybeBang . urlWrapText $ (betweenBangs <?> "Msg")+ _ucamAIssue <- noBang utcTimeParser+ _ucamAId <- fmap (fromMaybe "") . maybeBang . urlWrapText $ (betweenBangs <?> "Id")+ _ucamAUrl <- fmap (fromMaybe "") . maybeBang . urlWrapText $ (betweenBangs <?> "Url")+ _ucamAPrincipal <- parsePrincipal _ucamAStatus+ _ucamAPtags <- parsePtags _ucamAVer+ _ucamAAuth <- maybeBang $ authTypeParser+ _ucamASso <- parseSso _ucamAStatus _ucamAAuth+ _ucamALife <- maybeBang . fmap timePeriodFromSeconds $ decimal+ _ucamAParams <- A.decodeStrict . decodeUcamB64 . UcamB64 <$> betweenBangs <|> pure empty <?> "Parameters"+ pure AuthResponse{..}++noBang :: Parser b -> Parser b+noBang = (<* "!")++-- urlWrap :: Functor f => f ByteString -> f ByteString+-- urlWrap = fmap (urlDecode False)++urlWrapText :: Parser ByteString -> Parser Text+urlWrapText = (<?> "Url wrapping") . either (fail . show) pure . decodeUtf8' <=< fmap id -- (urlDecode False)++maybeBang :: Parser b -> Parser (Maybe b)+maybeBang = noBang . optional++parsePtags :: WLSVersion -> Parser [Ptag]+parsePtags WLS3 = noBang $ ptagParser `sepBy` ","+parsePtags _ = pure empty++parsePrincipal :: StatusCode -> Parser (Maybe Text)+parsePrincipal (statusCode . getStatus -> 200) = maybeBang . urlWrapText $ (betweenBangs <?> "Principal")+parsePrincipal _ = noBang empty <?> "Empty principal"++parseSso :: StatusCode -> Maybe AuthType -> Parser (Maybe (NonEmpty AuthType))+parseSso (statusCode . getStatus -> 200) Nothing = (maybeBang $ authTypeParser `sepBy1` ",") <?> "Sso"+parseSso _ _ = noBang (pure empty <?> "Empty Sso")++parseKidSig :: StatusCode -> Parser (Maybe KeyID, Maybe UcamBase64BS)+parseKidSig (statusCode . getStatus -> 200) =+ curry (pure *** pure)+ <$> (noBang kidParser <?> "The Kid")+ <*> (ucamB64parser <?> "The signature")+parseKidSig _ = (,)+ <$> (maybeBang kidParser <?> "The Kid")+ <*> (optional ucamB64parser <?> "The signature")++{-|+ The Ucam-Webauth protocol uses @!@ characters to separate the fields in the response. Any @!@+ characters in the data itself must be url encoded. The representations used in this module+ meet this criterion.++ TODO Add tests to verify.+-}+betweenBangs :: Parser ByteString+betweenBangs = takeWhile1 (/= '!')+ <?> "Missing field between !"++------------------------------------------------------------------------------+-- ** Helpers++{-|+ A parser for the 'WLSVersion', as used by the 'AuthResponse' parser.+-}+wlsVersionParser :: Parser WLSVersion+wlsVersionParser = choice+ [ "3" *> pure WLS3+ , "2" *> pure WLS2+ , "1" *> pure WLS1+ ]+ <?> "WLS version"++{-|+ A parser for 'AuthType' data+-}+authTypeParser :: Parser AuthType+authTypeParser = "pwd" *> pure Pwd+ <?> "AuthType"++{-|+ Parser representing a 'Ptag'+-}+ptagParser :: Parser Ptag+ptagParser = "current" *> pure Current+ <?> "PTag"++{-|+ A parser representing a typed 'Status' code within the protocol.+-}+responseCodeParser :: Parser StatusCode+responseCodeParser = toEnum <$> decimal+ <?> "Response code"++{-|+ The 'KeyID' can represent a restricted set of possible 'ByteString's, as per the protocol document,+ and this parser should only allow a valid representation.++ TODO Add tests to verify.+-}+kidParser :: Parser KeyID+kidParser = (<?> "Key id") $ do+ frst <- satisfy . inClass $ "1-9"+ rest <- (fmap catMaybes . A.count 7 . optional $ digit) <* (lookAhead . satisfy $ not . isDigit)+ pure (KeyID . B8.pack $ frst : rest)++{-|+ Using 'ucamTimeParser', work out the actual 'UTCTime' for further processing.++ If 'ucamTimeParser' succeeds it should always produce a valid result for 'parseTimeRFC3339'.+ As a result, 'parseTimeRFC3339' is extracted from the Maybe enviroment using 'fromMaybe' with+ 'error'.+-}+utcTimeParser :: Parser UTCTime+utcTimeParser = let er = error "Cannot parse time as RFC3339. There’s a bug in the parser." in+ zonedTimeToUTC . fromMaybe er . zonedUcamTime <$> ucamTimeParser+ <?> "UTCTime"++{-|+ This parses a 'ByteString' into a 'UcamTime'+-}+ucamTimeParser :: Parser UcamTime+ucamTimeParser = (<?> "UcamWebauth Time") $ do+ year <- stringUtf8 <$> A.count 4 digit+ month <- stringUtf8 <$> A.count 2 digit+ day <- stringUtf8 <$> A.count 2 digit <* "T"+ hour <- stringUtf8 <$> A.count 2 digit+ minute <- stringUtf8 <$> A.count 2 digit+ sec <- stringUtf8 <$> A.count 2 digit <* "Z"+ either (fail . show) (pure . UcamTime) . decodeUtf8' . BSL.toStrict . toLazyByteString . mconcat $+ [year, "-", month, "-", day, "T", hour, ":", minute, ":", sec, "Z"]++{-|+ A parser to represent a Ucam-Webauth variant base64–encoded 'ByteString' as a 'UcamBase64BS'+-}+ucamB64parser :: Parser UcamBase64BS+ucamB64parser = UcamB64 <$> takeWhile1 (ors [isAlphaNum, inClass "-._"])+ <?> "UcamWebauth Base 64 string"++------------------------------------------------------------------------------+-- * Helper functions++{-|+ Combines a list of predicates into a single predicate. /c.f./ 'any', which applies+ a single predicate to many items in a data structure.++ Simplifies to++ @ors :: ['Char' -> 'Bool'] -> 'Char' -> 'Bool'@+-}+ors+ :: (Traversable t, Applicative f)+ => t (f Bool) -> f Bool+ors = fmap or . sequenceA
+ src/UcamWebauth/WLS.hs view
@@ -0,0 +1,89 @@+{-|+Module : Protocol.UcamWebauth.WLS+Description : Encoding WLS responses in Ucam-Webauth protocol, from the University of Cambridge+Maintainer : David Baynard <ucamwebauth@baynard.me>++Encoding the WLS-Response contents.++<https://raven.cam.ac.uk/project/waa2wls-protocol.txt>++This is useful for test suite and more.++-}++{-# LANGUAGE+ PackageImports+ , OverloadedStrings+ , PartialTypeSignatures+ #-}++module UcamWebauth.WLS+ ( wlsEncode+ , wlsEncodeSign+ ) where++import "aeson" Data.Aeson (ToJSON)+import qualified "aeson" Data.Aeson as A+import "ucam-webauth-types" Data.ByteString.B64+import qualified "bytestring" Data.ByteString.Builder as B+import qualified "bytestring" Data.ByteString.Lazy as BSL+import "base" Data.Foldable (fold)+import "base" Data.List (intersperse)+import qualified "base" Data.List.NonEmpty as NE (intersperse)+import "base" Data.Maybe+import "text" Data.Text (Text)+import "text" Data.Text.Encoding+import "text" Data.Text.Encoding.Error (lenientDecode)+import "microlens" Lens.Micro+import "ucam-webauth-types" UcamWebauth.Data as X+import "ucam-webauth-types" UcamWebauth.Data.Internal++wlsEncode :: ToJSON a => AuthResponse a -> Text+wlsEncode = textBuilder . wlsEncode'++wlsEncodeSign :: ToJSON a => MaybeValidResponse a -> Text+wlsEncodeSign = textBuilder . wlsEncodeSign'++textBuilder :: B.Builder -> Text+textBuilder = decodeUtf8With lenientDecode . BSL.toStrict . B.toLazyByteString++wlsEncode' :: ToJSON a => AuthResponse a -> B.Builder+wlsEncode' r = mconcat . intersperse "!" $+ [ r ^. ucamAVer . to displayWLSVersion+ , r ^. ucamAStatus . to (B.stringUtf8 . show . fromEnum)+ , r ^. ucamAMsg . mTextEncoded+ , r ^. ucamAIssue . encoded (unUcamTime . ucamTime)+ , r ^. ucamAId . textEncoded+ , r ^. ucamAUrl . textEncoded+ , r ^. ucamAPrincipal . mTextEncoded+ , r ^. ucamAPtags . to (mconcat . intersperse "," . fmap displayPtag)+ , r ^. ucamAAuth . to (maybe "" displayAuthType)+ , r ^. ucamASso . to (maybe "" $ fold . NE.intersperse "," . fmap displayAuthType)+ , r ^. ucamALife . to (maybe "" $ B.stringUtf8 . show . secondsFromTimePeriod)+ , r ^. ucamAParams . to (maybe "" $ B.lazyByteString . unUcamB64L . encodeUcamB64L . A.encode)+ ]++wlsEncodeSign' :: ToJSON a => MaybeValidResponse a -> B.Builder+wlsEncodeSign' r = mconcat . intersperse "!" $+ [ r ^. ucamAResponse . to wlsEncode'+ , r ^. ucamAKid . to (maybe "" $ B.byteString . unKeyID)+ , r ^. ucamASig . to (maybe "" $ B.byteString . unUcamB64)+ ]++-- orBlank :: Getting r a B.Builder -> Getting r (Maybe a) B.Builder+-- orBlank g bcrb a = _++textEncoded :: Getting r Text B.Builder+textEncoded = encoded id++encoded :: ToJSON a => (a -> Text) -> Getting r a B.Builder+encoded f = to $ encodeUtf8Builder . f++mEncoded :: ToJSON a => (a -> Text) -> Getting r (Maybe a) B.Builder+mEncoded f = to $ encodeUtf8Builder . fromMaybe "" . fmap f++mTextEncoded :: Getting r (Maybe Text) B.Builder+mTextEncoded = mEncoded id++-- ucamTimeFormat :: (UTCTime )+-- ucamTimeFormat = B.stringUtf8 . formatTime defaultTimeLocale "%0Y%m%dT%H%M%SZ"
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/UcamWebauthSpec.hs view
@@ -0,0 +1,182 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE+ PackageImports+ , AllowAmbiguousTypes+ , FlexibleInstances+ , NamedFieldPuns+ , OverloadedLists+ , OverloadedStrings+ , QuasiQuotes+ , RecordWildCards+ , ScopedTypeVariables+ , TypeApplications+ , TypeOperators+ , TypeSynonymInstances+ #-}++module UcamWebauthSpec (spec) where++import "aeson" Data.Aeson.Types hiding ((.=))+import "base" Data.Bits+import qualified "bytestring" Data.ByteString as BS+import "ucam-webauth-types" Data.ByteString.B64+import qualified "bytestring" Data.ByteString.Char8 as B8+import "base" Data.Char+import "base" Data.Maybe+import "here" Data.String.Here+import "text" Data.Text (Text)+import "text" Data.Text.Encoding+import qualified "text" Data.Text as T+import "time" Data.Time+import "time-qq" Data.Time.QQ as Q+import "generic-random" Generic.Random+import "microlens" Lens.Micro+import "microlens-mtl" Lens.Micro.Mtl+import "hspec" Test.Hspec+import "hspec" Test.Hspec.QuickCheck+import "QuickCheck" Test.QuickCheck+import "quickcheck-instances" Test.QuickCheck.Instances ()+import "this" UcamWebauth+import "ucam-webauth-types" UcamWebauth.Data.Internal+import "http-api-data" Web.HttpApiData++spec :: Spec+spec = do+ describe "UcamWebauth" $ do+ it "should parse example response" $ do+ parseQueryParam @(AuthResponse Text) exampleResponseText `shouldBe` Right exampleResponse+ it "should parse example signed response" $ do+ parseQueryParam @(MaybeValidResponse Text) exampleSignedResponseText `shouldBe` Right exampleSignedResponse+ it "should produce example response" $ do+ toQueryParam @(AuthResponse Text) exampleResponse `shouldBe` exampleResponseText+ it "should produce example signed response" $ do+ toQueryParam @(MaybeValidResponse Text) exampleSignedResponse `shouldBe` exampleSignedResponseText+ modifyMaxSuccess (const 1000) $ prop_HttpApiData @(AuthResponse Text)+ modArgs $ prop_HttpApiData @(MaybeValidResponse Text)++modArgs :: SpecWith a -> SpecWith a+modArgs = modifyMaxSuccess (const 100) . modifyMaxDiscardRatio (const 1) . modifyMaxSize (const 1)++prop_HttpApiData+ :: forall a .+ ( Arbitrary a+ , Eq a+ , Show a+ , ToHttpApiData a+ , FromHttpApiData a+ )+ => Spec+prop_HttpApiData = prop "should serialize with HttpApiData correctly" $ \(h :: a) ->+ let qp = toQueryParam h in counterexample (T.unpack qp) $+ parseQueryParam qp === Right h++instance (ToJSON a, FromJSON a, Arbitrary a) => Arbitrary (MaybeValidResponse a) where+ arbitrary = do+ x <- genericArbitraryU `suchThat` \a -> and @[]+ [ notElem @[] (a ^. ucamASig) [Just "", Nothing]+ , isNothing (a ^. ucamASig) || isJust (a ^. ucamAKid)+ ]+ pure $ x &~ do+ ucamAToSign .= encodeUtf8 (toQueryParam (x ^. ucamAResponse))+ shrink = genericShrink++instance (ToJSON a, FromJSON a, Arbitrary a) => Arbitrary (AuthResponse a) where+ arbitrary = do+ x <- genericArbitraryU `suchThat` \a -> and @[]+ [ fromMaybe True $ a ^? ucamAPrincipal . _Just . to (T.all (/= '!'))+ , fromMaybe True $ a ^? ucamAMsg . _Just . to (T.all (/= '!'))+ , a ^. ucamAId . to (T.all (/= '!'))+ , a ^. ucamAUrl . to (T.all (/= '!'))+ , isJust (a ^. ucamAAuth) `xor` isJust (a ^. ucamASso)+ , a ^. ucamAPrincipal /= Just ""+ , a ^. ucamAMsg /= Just ""+ ]+ pure $ x &~ do+ ucamAIssue . dayTime %= fromInteger . round+ shrink = genericShrink++dayTime :: UTCTime `Lens'` DiffTime+dayTime f UTCTime{..} = (\x -> UTCTime{utctDayTime = x, ..}) <$> f utctDayTime+{-# INLINE dayTime #-}++instance Arbitrary KeyID where+ arbitrary = genericArbitraryU `suchThat` \(KeyID a) -> and @[]+ [ BS.length a & \x -> x >= 1 && x <= 8+ , B8.all isDigit a+ , B8.head a /= '0'+ ]+ shrink = genericShrink++instance Arbitrary WLSVersion where+ arbitrary = pure WLS3++instance Arbitrary StatusCode where+ arbitrary = pure Ok200++instance Arbitrary UcamBase64BS where+ arbitrary = encodeUcamB64 <$> arbitrary+ shrink = genericShrink++instance Arbitrary Ptag where+ arbitrary = genericArbitraryU+ shrink = genericShrink++instance Arbitrary AuthType where+ arbitrary = genericArbitraryU+ shrink = genericShrink++instance Arbitrary TimePeriod where+ arbitrary = timePeriodFromSeconds . secondsFromTimePeriod <$> genericArbitraryU `suchThat` (>= 0)++ shrink = genericShrink++exampleSignedResponse :: MaybeValidResponse Text+exampleSignedResponse = SignedAuthResponse+ { _ucamAResponse = AuthResponse+ { _ucamAVer = WLS3+ , _ucamAStatus = Ok200+ , _ucamAMsg = Nothing+ , _ucamAIssue = [utcIso8601ms| 2017-05-15T17:23:11 |]+ , _ucamAId = "oANAuhC9fZmMlZUPIm53y5vn"+ , _ucamAUrl = "http://localhost:3000/foo/query"+ , _ucamAPrincipal = Just "test0244"+ , _ucamAPtags = [Current]+ , _ucamAAuth = Nothing+ , _ucamASso = Just [Pwd]+ , _ucamALife = Just (timePeriodFromSeconds 30380)+ , _ucamAParams = Just "This is 100% of the data! And it’s really quite cool"+ }+ , _ucamAToSign = "3!200!!20170515T172311Z!oANAuhC9fZmMlZUPIm53y5vn!http://localhost:3000/foo/query!test0244!current!!pwd!30380!IlRoaXMgaXMgMTAwJSBvZiB0aGUgZGF0YSEgQW5kIGl04oCZcyByZWFsbHkgcXVpdGUgY29vbCI_"+ , _ucamAKid = Just "901"+ , _ucamASig = Just+ (UcamB64+ { unUcamB64 = "RzC9KZWALCSeK0n9885X4zzemHizuj8K.NOpt.n1hfRCTE2ZBgvJ-fBvT-PaL80cSFGpyCJgt9LvM4-peJzcidoKC6zhBEvG0QnlqWTLsphbIA0JmBRiOoeqyLYRVGwDEdLdacdsQRM.u7bik.enhbuN1-aIQCOdB5MutxtYiu4_"+ }+ )+ }++exampleResponse :: AuthResponse Text+exampleResponse = AuthResponse+ { _ucamAVer = WLS3+ , _ucamAStatus = Ok200+ , _ucamAMsg = Nothing+ , _ucamAIssue = [utcIso8601ms| 2017-05-15T17:23:11 |]+ , _ucamAId = "oANAuhC9fZmMlZUPIm53y5vn"+ , _ucamAUrl = "http://localhost:3000/foo/query"+ , _ucamAPrincipal = Just "test0244"+ , _ucamAPtags = [Current]+ , _ucamAAuth = Nothing+ , _ucamASso = Just [Pwd]+ , _ucamALife = Just (timePeriodFromSeconds 30380)+ , _ucamAParams = Just "This is 100% of the data! And it’s really quite cool"+ }++exampleSignedResponseText :: Text+exampleSignedResponseText = exampleResponseText <> [here|+ !901!RzC9KZWALCSeK0n9885X4zzemHizuj8K.NOpt.n1hfRCTE2ZBgvJ-fBvT-PaL80cSFGpyCJgt9LvM4-peJzcidoKC6zhBEvG0QnlqWTLsphbIA0JmBRiOoeqyLYRVGwDEdLdacdsQRM.u7bik.enhbuN1-aIQCOdB5MutxtYiu4_+|]++exampleResponseText :: Text+exampleResponseText = [here|+ 3!200!!20170515T172311Z!oANAuhC9fZmMlZUPIm53y5vn!http://localhost:3000/foo/query!test0244!current!!pwd!30380!IlRoaXMgaXMgMTAwJSBvZiB0aGUgZGF0YSEgQW5kIGl04oCZcyByZWFsbHkgcXVpdGUgY29vbCI_+|]
+ ucam-webauth.cabal view
@@ -0,0 +1,110 @@+cabal-version: 2.2+name: ucam-webauth+version: 0.1.0.0+license: (BSD-3-Clause OR Apache-2.0)+license-file: LICENSE+copyright: 2018 David Baynard+maintainer: David Baynard <ucamwebauth@baynard.me>+author: David Baynard <ucamwebauth@baynard.me>+homepage: https://github.com/dbaynard/UcamWebauth#readme+bug-reports: https://github.com/dbaynard/UcamWebauth/issues+synopsis: The Ucam-Webauth protocol, used by Raven+description:+ An implementation of the Ucam-Webauth protocol, as used by the University of Cambridge’s+ Raven authentication service.+category: Web+build-type: Simple+extra-source-files:+ Changelog.md+ README.md++source-repository head+ type: git+ location: https://github.com/dbaynard/UcamWebauth++flag dev+ description:+ Compile for development+ default: False+ manual: True++library+ exposed-modules:+ UcamWebauth+ UcamWebauth.Internal+ UcamWebauth.Parser+ UcamWebauth.WLS+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -Wall -Wincomplete-uni-patterns+ -Wincomplete-record-updates -Wcompat -Wnoncanonical-monad-instances+ -Wnoncanonical-monadfail-instances+ build-depends:+ aeson >=1.2 && <1.5,+ attoparsec >=0.13.2.2 && <0.14,+ base >=4.11.0.0 && <4.12,+ bytestring >=0.10.8.2 && <0.11,+ containers >=0.5.11.0 && <0.6,+ cryptonite ==0.25.*,+ errors >=2.3.0 && <2.4,+ http-api-data >=0.3.8.1 && <0.4,+ http-types >=0.12.1 && <0.13,+ microlens >=0.4.9.1 && <0.5,+ microlens-mtl >=0.1.11.1 && <0.2,+ mtl >=2.2.2 && <2.3,+ parser-combinators >=1.0.0 && <1.1,+ pem >=0.2.4 && <0.3,+ text >=0.11 && <1.2.3.0 || >=1.2.3.1 && <1.3,+ time >=1.9.2 && <1.10,+ ucam-webauth-types >=0.1.0.0 && <0.2,+ x509 >=1.7.4 && <1.8+ + if flag(dev)+ ghc-options: -ddump-minimal-imports++test-suite test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ build-tool-depends: hspec-discover:hspec-discover -any+ hs-source-dirs: src test+ other-modules:+ UcamWebauth+ UcamWebauth.Internal+ UcamWebauth.Parser+ UcamWebauth.WLS+ UcamWebauthSpec+ Paths_ucam_webauth+ autogen-modules:+ Paths_ucam_webauth+ default-language: Haskell2010+ ghc-options: -Wall -Wincomplete-uni-patterns+ -Wincomplete-record-updates -Wcompat -Wnoncanonical-monad-instances+ -Wnoncanonical-monadfail-instances -Wall+ build-depends:+ QuickCheck >=2.12.4 && <2.13,+ aeson >=1.2 && <1.5,+ attoparsec >=0.13.2.2 && <0.14,+ base >=4.11.0.0 && <4.12,+ bytestring >=0.10.8.2 && <0.11,+ containers >=0.5.11.0 && <0.6,+ cryptonite ==0.25.*,+ errors >=2.3.0 && <2.4,+ generic-random >=1.2.0.0 && <1.3,+ here >=1.2.13 && <1.3,+ hspec >=2.0.0 && <2.6,+ http-api-data >=0.3.8.1 && <0.4,+ http-types >=0.12.1 && <0.13,+ microlens >=0.4.9.1 && <0.5,+ microlens-mtl >=0.1.11.1 && <0.2,+ mtl >=2.2.2 && <2.3,+ parser-combinators >=1.0.0 && <1.1,+ pem >=0.2.4 && <0.3,+ quickcheck-instances >=0.3.19 && <0.4,+ text >=0.11 && <1.2.3.0 || >=1.2.3.1 && <1.3,+ time >=1.9.2 && <1.10,+ time-qq >=0.0.1.0 && <0.1,+ ucam-webauth-types >=0.1.0.0 && <0.2,+ x509 >=1.7.4 && <1.8+ + if flag(dev)+ ghc-options: -ddump-minimal-imports