packages feed

u2f 0.1.0.0 → 0.1.0.1

raw patch · 4 files changed

+208/−140 lines, 4 filesdep ~aesondep ~asn1-encodingdep ~asn1-types

Dependency ranges changed: aeson, asn1-encoding, asn1-types, base, base64-bytestring, binary, bytestring, cryptohash, cryptonite, hspec, text

Files

src/U2F.hs view
@@ -1,30 +1,62 @@ {-# LANGUAGE DeriveGeneric #-} -module U2F where+{- |+  __HOW TO USE__ -import GHC.Generics-import Data.Bits+  To Register -import Data.Either.Unwrap+    - Generate yourself a Request, consisting of your site/service uri, u2f version number, etc, send it to the client. +    - Assuming the client returned a registration response (Registration), parse it with parseRegistration.++    - Use verifyRegistration Request Registration to verify that the Registration is valid. (Challenge bytes match, were signed by key described in cert)++    - Stash the publicKey and keyHandle somewhere, so you can use them for signin. verifyRegistration returns a Request, with added keyHandle, for convenience.++  To Signin++    - Make a Request.++    - Parse whatever signin json you have with parseSignin.++    - Dig out the publicKey for the relevant keyHandle.++    - Verify signin with verifySignin publicKey Request Signin++-}+module U2F+  (+    parseRequest,+    parseRegistration,+    parseRegistrationData,+    verifyRegistration,+    parseSignin,+    parseClientData,+    verifySignin,+    formatOutputBase64+  )where+import U2F.Types++import Data.Bits++import Data.ASN1.BitArray+import Data.ASN1.Encoding import Data.ASN1.BinaryEncoding import Data.ASN1.Types+ import qualified Data.ByteString.Lazy.Char8 as LBS import qualified Data.ByteString.Char8 as BS -import Data.Text.Encoding (encodeUtf8)-import Data.Aeson ((.:), (.:?), decode, FromJSON(..),-  ToJSON(..), Value(..), genericParseJSON, defaultOptions)+import Data.Aeson (decode) import Data.Binary.Get-import Data.Aeson.Types-import Control.Applicative ((<$>), (<*>))-import Data.ASN1.BitArray-import Data.ASN1.Encoding+ import Data.ByteString (pack) import Data.ByteString.Base64.URL (encode, decodeLenient)++import Data.List+ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8, decodeUtf8)-import Data.List  import qualified Crypto.Hash.SHA256 as SHA256 @@ -35,83 +67,23 @@ import qualified Crypto.PubKey.ECC.ECDSA as ECDSA import Crypto.Hash.Algorithms -{--  HOW TO USE-  ==========-  To Register-    - Generate yourself a Request, consisting of your site/service uri, u2f version number, etc, send it to the client.-    - Assuming the client returned a registration response (Registration), parse it with parseRegistration.-    - Use verifyRegistration Request Registration to verify that the Registration is valid. (Challenge bytes match, were signed by key described in cert)-    - Stash the publicKey and keyHandle somewhere, so you can use them for signin. verifyRegistration returns a Request, with added keyHandle, for convenience.-  To Signin-    - Make a Request.-    - Parse whatever signin json you have with parseSignin.-    - Dig out the publicKey for the relevant keyHandle.-    - Verify signin with verifySignin publicKey Request Signin--}---- Curve+-- | The U2F Spec (currently) exclusively supports use of the SEC p256r Curve+ourCurve :: Curve ourCurve = getCurveByName SEC_p256r1 --- Errors-data U2FError =-  RegistrationParseError |-  RegistrationDataParseError |-  RegistrationCertificateParseError |-  PubKeyParsingError |-  SignatureParseError |-  ChallengeMismatchError |-  FailedVerificationError |-  SigninParseError |-  ClientDataParseError |-  RequestParseError-  deriving (Show, Eq)--data Request = Request {-  appId :: T.Text,-  version :: T.Text,-  challenge :: T.Text,-  keyHandle :: Maybe T.Text-} deriving (Show, Generic, Eq)-instance FromJSON Request-instance ToJSON Request---- Registration Flow ---data Registration = Registration {-  registration_registrationData :: T.Text,-  registration_challenge :: T.Text,-  registration_version :: Maybe T.Text,-  registration_appId :: T.Text,-  registration_clientData :: T.Text,-  registration_sessionID :: Maybe T.Text-} deriving (Show, Generic)-instance ToJSON Registration where-  toJSON = genericToJSON defaultOptions {-                fieldLabelModifier = Prelude.drop 13 }--instance FromJSON Registration where-  parseJSON = genericParseJSON defaultOptions {-                fieldLabelModifier = Prelude.drop 13 }---data RegistrationData = RegistrationData {-  registrationData_reserved :: BS.ByteString,-  registrationData_publicKey :: BS.ByteString,-  registrationData_keyHandle :: BS.ByteString,-  registrationData_certificate :: BS.ByteString,-  registrationData_signature :: BS.ByteString-} deriving (Show, Generic)-+-- | Parses Registration or Signin Request JSON parseRequest :: String -> Either U2FError Request parseRequest x = case (Data.Aeson.decode (LBS.pack x) :: Maybe Request) of   Just request -> Right request   Nothing -> Left RequestParseError +-- | Parses Registration response JSON parseRegistration :: String -> Either U2FError Registration parseRegistration x = case (Data.Aeson.decode (LBS.pack x) :: Maybe Registration) of   Just registration -> Right registration   Nothing -> Left RegistrationParseError +-- | Parses base64-encoded bytestring in Registration response parseRegistrationData :: BS.ByteString -> Either U2FError RegistrationData parseRegistrationData r = Right $ runGet unpackRegistrationData ( LBS.fromStrict $ decodeLenient r) @@ -124,7 +96,8 @@  findPubKey :: Foldable t => t ASN1 -> Maybe ECDSA.PublicKey findPubKey parsedCert = case (find pubKeyShape parsedCert) of-  Just (BitString (BitArray len x)) -> parsePublicKey $ BS.tail x+  -- Eventually check to make sure this is not compressed, in right format+  Just (BitString (BitArray _ x)) -> parsePublicKey $ BS.tail x   _ -> Nothing  pubKeyShape :: ASN1 -> Bool@@ -143,9 +116,10 @@   publicKey <- pure $ registrationData_publicKey registrationData   pure $ getSignatureBase appId clientData keyHandle publicKey +-- | Verifies that Registration is a valid response to the Request verifyRegistration :: Request -> Registration -> Either U2FError Request verifyRegistration request registration = do-  challengesEqual <- u2fComparator (challenge request) (registration_challenge registration) ChallengeMismatchError+  _ <- u2fComparator (challenge request) (registration_challenge registration) ChallengeMismatchError   registrationData <- parseRegistrationData $ encodeUtf8 $ registration_registrationData registration   pkey <- getPubKeyFromCertificate $ registrationData_certificate registrationData   signature <- parseSignature $ registrationData_signature registrationData@@ -154,48 +128,23 @@     True -> Right (request {keyHandle = Just $ formatOutputBase64 $ registrationData_keyHandle registrationData})     False -> Left FailedVerificationError --- Signin Flow ---data Signin = Signin {-  signin_keyHandle :: T.Text,-  signin_clientData :: T.Text,-  signin_signatureData :: T.Text-} deriving (Show, Generic, Eq)--instance FromJSON Signin where-  parseJSON = genericParseJSON defaultOptions {-                fieldLabelModifier = Prelude.drop 7 }--data ClientData = ClientData {-  clientData_typ :: T.Text,-  clientData_challenge :: T.Text,-  clientData_origin :: T.Text,-  clientData_cid_pubkey :: T.Text-  } deriving (Show, Generic, Eq)--instance FromJSON ClientData where-  parseJSON = genericParseJSON defaultOptions {-                fieldLabelModifier = Prelude.drop 11 }--data SignatureData = SignatureData {-  signatureData_userPresenceFlag :: BS.ByteString,-  signatureData_counter :: BS.ByteString,-  signatureData_signature :: BS.ByteString-} deriving (Show, Generic)-+-- | Parses Signin response JSON parseSignin :: String -> Either U2FError Signin parseSignin x = case (Data.Aeson.decode (LBS.pack x) :: Maybe Signin) of   Just signin -> Right signin   Nothing -> Left SigninParseError +-- | Parses base64-encoded client data bytestring inside Signin response parseClientData :: BS.ByteString -> Either U2FError ClientData parseClientData x = case (Data.Aeson.decode (LBS.fromStrict $ decodeLenient x) :: Maybe ClientData) of   Just clientData -> Right clientData   Nothing -> Left ClientDataParseError +-- | Verifies that Signin response is valid given saved pubkey bytestring, request verifySignin :: BS.ByteString -> Request -> Signin -> Either U2FError Bool verifySignin savedPubkey request signin = do   clientData <- parseClientData $ encodeUtf8 $ signin_clientData signin-  challengesEqual <- u2fComparator (challenge request) (clientData_challenge clientData) ChallengeMismatchError+  _ <- u2fComparator (challenge request) (clientData_challenge clientData) ChallengeMismatchError   signatureData <- parseSignatureData $ encodeUtf8 $ signin_signatureData signin   signature <- parseSignature $ signatureData_signature signatureData   signatureBase <- getSigninSignatureBase request signin signatureData@@ -207,8 +156,6 @@     True -> Right True     False -> Left FailedVerificationError --- Other stuff- parseSignatureData :: BS.ByteString -> Either U2FError SignatureData parseSignatureData s = Right $ runGet unpackSignatureData ( LBS.fromStrict $ decodeLenient s) @@ -228,10 +175,10 @@ parsePublicKey :: BS.ByteString -> Maybe ECDSA.PublicKey parsePublicKey keyByteString = case P256.pointFromBinary keyByteString of   CryptoPassed key -> Just $ ECDSA.PublicKey ourCurve $ Point (fst $ P256.pointToIntegers key) (snd $ P256.pointToIntegers key)-  CryptoFailed err -> Nothing+  CryptoFailed _ -> Nothing --- URL-friendly base64 encoding may or may not contain padding. Delete it here--- https://tools.ietf.org/html/rfc4648#section-3.2+-- | URL-friendly base64 encoding may or may not contain padding. (https://tools.ietf.org/html/rfc4648#section-3.2).+--   We remove it here. formatOutputBase64 :: BS.ByteString -> T.Text formatOutputBase64 byteString = T.replace (T.pack "=") (T.pack "") (decodeUtf8 $ encode byteString) 
+ src/U2F/Types.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE DeriveGeneric #-}++module U2F.Types where++import GHC.Generics+import qualified Data.Text as T+import qualified Data.ByteString.Char8 as BS+import Data.Aeson ((.:), (.:?), decode, FromJSON(..),+  ToJSON(..), Value(..), genericParseJSON, genericToJSON,  defaultOptions)+import Data.Aeson.Types (fieldLabelModifier)++data U2FError =+  RegistrationParseError            |+  RegistrationDataParseError        |+  RegistrationCertificateParseError |+  PubKeyParsingError                |+  SignatureParseError               |+  ChallengeMismatchError            |+  FailedVerificationError           |+  SigninParseError                  |+  ClientDataParseError              |+  RequestParseError                 |+  RegisterRequestParseError+  deriving (Show, Eq)++data Transport =  BT  | -- ^ Bluetooth Classic (Bluetooth BR/EDR)+                  BLE | -- ^ Bluetooth Low Energy (Bluetooth Smart)+                  NFC | -- ^ Near-Field Communications+                  USB   -- ^ USB HID (Human Interface Device)+  deriving (Show, Eq)++-- | Generic request for old version of protocol. Probably want to remove+data Request = Request {+  appId :: T.Text,+  version :: T.Text,+  challenge :: T.Text,+  keyHandle :: Maybe T.Text+} deriving (Show, Generic, Eq)+instance FromJSON Request+instance ToJSON Request++data RegisterRequest = RegisterRequest {+  registerRequest_version :: T.Text, -- Will probably be 'U2F_V2'+  registerRequest_challenge :: T.Text+} deriving (Show, Generic)+instance ToJSON RegisterRequest where+  toJSON = genericToJSON defaultOptions {+                fieldLabelModifier = Prelude.drop 16 }+instance FromJSON RegisterRequest where+  parseJSON = genericParseJSON defaultOptions {+                fieldLabelModifier = Prelude.drop 16 }++data RegisteredKey = RegisteredKey {+  registeredKey_version :: T.Text,+  registeredKey_keyHandle :: T.Text,+  registeredKey_transports :: Maybe [Transport],+  registeredKey_appId :: Maybe T.Text+} deriving (Show, Generic, Eq)++-- Register Flow --++data Registration = Registration {+  registration_registrationData :: T.Text,+  registration_challenge :: T.Text,+  registration_version :: Maybe T.Text,+  registration_appId :: T.Text,+  registration_clientData :: T.Text,+  registration_sessionID :: Maybe T.Text+} deriving (Show, Generic)+instance ToJSON Registration where+  toJSON = genericToJSON defaultOptions {+                fieldLabelModifier = Prelude.drop 13 }++instance FromJSON Registration where+  parseJSON = genericParseJSON defaultOptions {+                fieldLabelModifier = Prelude.drop 13 }+++data RegistrationData = RegistrationData {+  registrationData_reserved :: BS.ByteString,+  registrationData_publicKey :: BS.ByteString,+  registrationData_keyHandle :: BS.ByteString,+  registrationData_certificate :: BS.ByteString,+  registrationData_signature :: BS.ByteString+} deriving (Show, Generic)++-- Signin Flow --+data Signin = Signin {+  signin_keyHandle :: T.Text,+  signin_clientData :: T.Text,+  signin_signatureData :: T.Text+} deriving (Show, Generic, Eq)++instance FromJSON Signin where+  parseJSON = genericParseJSON defaultOptions {+                fieldLabelModifier = Prelude.drop 7 }++data ClientData = ClientData {+  clientData_typ :: T.Text,+  clientData_challenge :: T.Text,+  clientData_origin :: T.Text,+  clientData_cid_pubkey :: T.Text+  } deriving (Show, Generic, Eq)++instance FromJSON ClientData where+  parseJSON = genericParseJSON defaultOptions {+                fieldLabelModifier = Prelude.drop 11 }++data SignatureData = SignatureData {+  signatureData_userPresenceFlag :: BS.ByteString,+  signatureData_counter :: BS.ByteString,+  signatureData_signature :: BS.ByteString+} deriving (Show, Generic)
tests/test.hs view
@@ -1,26 +1,29 @@ import Test.Hspec-import Control.Exception (evaluate) import U2F-import Data.Maybe+import U2F.Types+import Data.Either.Unwrap (fromRight) import qualified Data.ByteString.Char8 as BS import qualified Data.Text as T +rawRegRequest :: String rawRegRequest = "{\"appId\":\"https://localhost:4000\",\"version\":\"U2F_V2\",\"challenge\":\"7uy5uxqHXeCGg7K1cWmO3bVlr_KY1U5dQkPP_wOvlWg\",\"keyHandle\":\"9Hgjem2ZlnKKuKVnHepmuSzMHnoyCX0idccYCwztgtpXllNSr-xv_oSuQnyVCN8HYa5JgIsawOxhtp0CIIcBGQ\"}"+registrationRequest :: Request registrationRequest = Request (T.pack "https://localhost:4000") (T.pack "U2F_V2") (T.pack "7uy5uxqHXeCGg7K1cWmO3bVlr_KY1U5dQkPP_wOvlWg") Nothing-registrationResponse = justRight $ parseRegistration "{\"registrationData\":\"BQQ65jfw5zT6MqdV81UYQ6btGV-dK8wRyxAGuYcFAKvFp1HubXnyI9wG5Vw8zdmE8sjBNK58GNrJ3woQ-e_nnV9kQPR4I3ptmZZyirilZx3qZrkszB56Mgl9InXHGAsM7YLaV5ZTUq_sb_6ErkJ8lQjfB2GuSYCLGsDsYbadAiCHARkwggJEMIIBLqADAgECAgR4wN8OMAsGCSqGSIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMjAyNTkwNTkzNDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLW4cVyD_f4OoVxFd6yFjfSMF2_eh53K9Lg9QNMg8m-t5iX89_XIr9g1GPjbniHsCDsYRYDHF-xKRwuWim-6P2-jOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgUgMAsGCSqGSIb3DQEBCwOCAQEAPvar9kqRawv5lJON3JU04FRAAmhWeKcsQ6er5l2QZf9h9FHOijru2GaJ0ZC5UK8AelTRMe7wb-JrTqe7PjK3kgWl36dgBDRT40r4RMN81KhfjFwthw4KKLK37UQCQf2zeSsgdrDhivqbQy7u_CZYugkFxBskqTxuyLum1W8z6NZT189r1QFUVaJll0D33MUcwDFgnNA-ps3pOZ7KCHYykHY_tMjQD1aQaaElSQBq67BqIaIU5JmYN7Qp6B1-VtM6VJLdOhYcgpOVQIGqfu90nDpWPb3X26OVzEc-RGltQZGFwkN6yDrAZMHL5HIn_3obd8fV6gw2fUX2ML2ZjVmybjBEAiBGwwt4P70-8E1KmmKQBtVQkvi-w16gSYLECB68b8nDNgIgCiRB1ATDXuWQ7m2DfNnsEq3bs3haITTa4ssHWB8PG-0\",\"challenge\":\"7uy5uxqHXeCGg7K1cWmO3bVlr_KY1U5dQkPP_wOvlWg\",\"version\":\"U2F_V2\",\"appId\":\"https://localhost:4000\",\"sessionId\":\"444\",\"clientData\":\"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCIsImNoYWxsZW5nZSI6Ijd1eTV1eHFIWGVDR2c3SzFjV21PM2JWbHJfS1kxVTVkUWtQUF93T3ZsV2ciLCJvcmlnaW4iOiJodHRwczovL2xvY2FsaG9zdDo0MDAwIiwiY2lkX3B1YmtleSI6InVudXNlZCJ9\"}"-registrationGoodSig = BS.pack "0D\STX F\195\vx?\189>\240MJ\154b\144\ACK\213P\146\248\190\195^\160I\130\196\b\RS\188o\201\195\&6\STX \n$A\212\EOT\195^\229\144\238m\131|\217\236\DC2\173\219\179xZ!4\218\226\203\aX\US\SI\ESC\237"-registrationGoodSigBase = BS.pack "\NUL\219\214S\135\176*\251\174\&8J\132g\177\209\ETX\192y\136s/L\136si\RSA\255\140\184/%\129Q\208\243\CAN!\204\&1\202\144j\153\246\&4\171\SOH\251@\165\244%\202=\ETB>\217\160\a\STX!\158\246W\244x#zm\153\150r\138\184\165g\GS\234f\185,\204\RSz2\t}\"u\199\CAN\v\f\237\130\218W\150SR\175\236o\254\132\174B|\149\b\223\aa\174I\128\139\SUB\192\236a\182\157\STX \135\SOH\EM\EOT:\230\&7\240\231\&4\250\&2\167U\243U\CANC\166\237\EM_\157+\204\DC1\203\DLE\ACK\185\135\ENQ\NUL\171\197\167Q\238my\242#\220\ACK\229\\<\205\217\132\242\200\193\&4\174|\CAN\218\201\223\n\DLE\249\239\231\157_d"+registrationResponse :: Registration+registrationResponse = fromRight $ parseRegistration "{\"registrationData\":\"BQQ65jfw5zT6MqdV81UYQ6btGV-dK8wRyxAGuYcFAKvFp1HubXnyI9wG5Vw8zdmE8sjBNK58GNrJ3woQ-e_nnV9kQPR4I3ptmZZyirilZx3qZrkszB56Mgl9InXHGAsM7YLaV5ZTUq_sb_6ErkJ8lQjfB2GuSYCLGsDsYbadAiCHARkwggJEMIIBLqADAgECAgR4wN8OMAsGCSqGSIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMjAyNTkwNTkzNDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLW4cVyD_f4OoVxFd6yFjfSMF2_eh53K9Lg9QNMg8m-t5iX89_XIr9g1GPjbniHsCDsYRYDHF-xKRwuWim-6P2-jOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgUgMAsGCSqGSIb3DQEBCwOCAQEAPvar9kqRawv5lJON3JU04FRAAmhWeKcsQ6er5l2QZf9h9FHOijru2GaJ0ZC5UK8AelTRMe7wb-JrTqe7PjK3kgWl36dgBDRT40r4RMN81KhfjFwthw4KKLK37UQCQf2zeSsgdrDhivqbQy7u_CZYugkFxBskqTxuyLum1W8z6NZT189r1QFUVaJll0D33MUcwDFgnNA-ps3pOZ7KCHYykHY_tMjQD1aQaaElSQBq67BqIaIU5JmYN7Qp6B1-VtM6VJLdOhYcgpOVQIGqfu90nDpWPb3X26OVzEc-RGltQZGFwkN6yDrAZMHL5HIn_3obd8fV6gw2fUX2ML2ZjVmybjBEAiBGwwt4P70-8E1KmmKQBtVQkvi-w16gSYLECB68b8nDNgIgCiRB1ATDXuWQ7m2DfNnsEq3bs3haITTa4ssHWB8PG-0\",\"challenge\":\"7uy5uxqHXeCGg7K1cWmO3bVlr_KY1U5dQkPP_wOvlWg\",\"version\":\"U2F_V2\",\"appId\":\"https://localhost:4000\",\"sessionId\":\"444\",\"clientData\":\"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCIsImNoYWxsZW5nZSI6Ijd1eTV1eHFIWGVDR2c3SzFjV21PM2JWbHJfS1kxVTVkUWtQUF93T3ZsV2ciLCJvcmlnaW4iOiJodHRwczovL2xvY2FsaG9zdDo0MDAwIiwiY2lkX3B1YmtleSI6InVudXNlZCJ9\"}" +signinRequest :: Request signinRequest = Request (T.pack "https://localhost:4000") (T.pack "U2F_V2") (T.pack "7uy5uxqHXeCGg7K1cWmO3bVlr_KY1U5dQkPP_wOvlWg") (Just $ T.pack "9Hgjem2ZlnKKuKVnHepmuSzMHnoyCX0idccYCwztgtpXllNSr-xv_oSuQnyVCN8HYa5JgIsawOxhtp0CIIcBGQ")-signinResponse = justRight $ parseSignin "{\"keyHandle\":\"9Hgjem2ZlnKKuKVnHepmuSzMHnoyCX0idccYCwztgtpXllNSr-xv_oSuQnyVCN8HYa5JgIsawOxhtp0CIIcBGQ\",\"clientData\":\"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoiN3V5NXV4cUhYZUNHZzdLMWNXbU8zYlZscl9LWTFVNWRRa1BQX3dPdmxXZyIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjQwMDAiLCJjaWRfcHVia2V5IjoidW51c2VkIn0\",\"signatureData\":\"AQAAAAAwRgIhAI4jLgLewiaFzuyyuaxlToF1OHmkItaOeASUtIStic5HAiEA8X16pOe_Sugk8O8AAh2iQdlx_98cR9UwwCwDmr_bbXo\"}"+signinResponse :: Signin+signinResponse = fromRight $ parseSignin "{\"keyHandle\":\"9Hgjem2ZlnKKuKVnHepmuSzMHnoyCX0idccYCwztgtpXllNSr-xv_oSuQnyVCN8HYa5JgIsawOxhtp0CIIcBGQ\",\"clientData\":\"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoiN3V5NXV4cUhYZUNHZzdLMWNXbU8zYlZscl9LWTFVNWRRa1BQX3dPdmxXZyIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjQwMDAiLCJjaWRfcHVia2V5IjoidW51c2VkIn0\",\"signatureData\":\"AQAAAAAwRgIhAI4jLgLewiaFzuyyuaxlToF1OHmkItaOeASUtIStic5HAiEA8X16pOe_Sugk8O8AAh2iQdlx_98cR9UwwCwDmr_bbXo\"}"+signinSavedPubkey :: BS.ByteString signinSavedPubkey = BS.pack "\EOT:\230\&7\240\231\&4\250\&2\167U\243U\CANC\166\237\EM_\157+\204\DC1\203\DLE\ACK\185\135\ENQ\NUL\171\197\167Q\238my\242#\220\ACK\229\\<\205\217\132\242\200\193\&4\174|\CAN\218\201\223\n\DLE\249\239\231\157_d"-justRight (Right x) = x  main :: IO () main = hspec $ do   describe "Request/Response Parsing" $ do     it "should work for registration requests" $ do-      regRequest <- pure $ justRight $ parseRequest rawRegRequest+      regRequest <- pure $ fromRight $ parseRequest rawRegRequest       (appId regRequest) `shouldBe` (T.pack "https://localhost:4000")    describe "Registration Flow" $ do@@ -34,7 +37,7 @@       registrationVerificationSuccess <- return $ do             verifyRegistration registrationRequest registrationResponse       --TODO: Find a better way to test for this-      (registrationVerificationSuccess) `shouldBe` (Right $ justRight registrationVerificationSuccess)+      (registrationVerificationSuccess) `shouldBe` (Right $ fromRight registrationVerificationSuccess)    describe "Signin Flow" $ do     it "should check that request and response challenges are equivalent" $ do@@ -44,7 +47,6 @@       signinVerificationFail `shouldBe` (Left ChallengeMismatchError)      it "should validate properly formed request and response" $ do-      signinVerificationSuccess <- return $ do-            verifySignin signinSavedPubkey signinRequest signinResponse+      signinVerificationSuccess <- return $ verifySignin signinSavedPubkey signinRequest signinResponse       --TODO: Find a better way to test for this-      (signinVerificationSuccess) `shouldBe` (Right $ justRight signinVerificationSuccess)+      (signinVerificationSuccess) `shouldBe` (Right $ fromRight signinVerificationSuccess)
u2f.cabal view
@@ -2,7 +2,7 @@ -- see http://haskell.org/cabal/users-guide/  name:                u2f-version:             0.1.0.0+version:             0.1.0.1 synopsis:            Haskell Universal Two Factor helper toolbox library thing description:         Library useful for server-side U2F Registration and Signin flows. homepage:            https://github.com/EButlerIV/u2f@@ -15,30 +15,36 @@ build-type:          Simple extra-source-files:  ChangeLog.md, README.md cabal-version:       >=1.10+tested-with: GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.3, GHC == 8.0.1, GHC == 8.0.2  library   exposed-modules:     U2F+                     , U2F.Types+  ghc-options: -Wall   -- other-modules:   -- other-extensions:-  build-depends:       base >=4.8 && <4.9-                       , cryptonite >= 0.17-                       , binary >= 0.8.4.0-                       , text-                       , asn1-encoding-                       , asn1-types-                       , base64-bytestring-                       , cryptohash-                       , aeson-                       , bytestring-                       , either-unwrap+  build-depends:       base >= 4.8 && < 4.11+                       , cryptonite >= 0.17 && < 0.21+                       , binary >= 0.8.4.0 && < 0.9+                       , text >= 1.2 && < 1.3+                       , asn1-encoding >= 0.9 && < 0.10+                       , asn1-types >= 0.3.2 && < 0.4+                       , base64-bytestring >= 1.0.0.1 && < 1.0.1.0+                       , cryptohash >= 0.11 && < 0.12+                       , aeson >= 1.0 && < 1.1+                       , bytestring >= 0.10 && < 0.11   hs-source-dirs:      src   default-language:    Haskell2010 -test-suite test+test-suite hspec-suite   type:       exitcode-stdio-1.0-  main-is:    tests/test.hs+  main-is:    test.hs+  ghc-options: -Wall+  hs-source-dirs: tests+  default-language:    Haskell2010   build-depends:  base-                , hspec+                , hspec >= 2.2 && < 2.3                 , u2f                 , text+                , either-unwrap                 , bytestring