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 +60/−113
- src/U2F/Types.hs +113/−0
- tests/test.hs +14/−12
- u2f.cabal +21/−15
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