diff --git a/src/Web/WebAuthn.hs b/src/Web/WebAuthn.hs index d7e38e4..423b735 100644 --- a/src/Web/WebAuthn.hs +++ b/src/Web/WebAuthn.hs @@ -209,7 +209,7 @@ verifyClientTokenBinding _ _ = pure () verifyAuthenticatorData :: RelyingParty -> ByteString -> Bool -> Either VerificationFailure AuthenticatorData verifyAuthenticatorData rp adRaw verificationRequired = do ad <- first (const MalformedAuthenticatorData) (C.runGet parseAuthenticatorData adRaw) - hash (rpId rp) == rpIdHash ad ?? MismatchedRPID + hash (rpId (rp :: RelyingParty)) == rpIdHash ad ?? MismatchedRPID userPresent ad ?? UserNotPresent not verificationRequired || userVerified ad ?? UserUnverified pure ad diff --git a/src/Web/WebAuthn/AndroidSafetyNet.hs b/src/Web/WebAuthn/AndroidSafetyNet.hs index e5740e2..75322f0 100644 --- a/src/Web/WebAuthn/AndroidSafetyNet.hs +++ b/src/Web/WebAuthn/AndroidSafetyNet.hs @@ -1,86 +1,85 @@ - {-# LANGUAGE OverloadedStrings #-} -module Web.WebAuthn.AndroidSafetyNet ( - decode, - verify -) where - -import qualified Data.Aeson as J -import Data.ByteString (ByteString) -import Data.Text (pack) -import Data.Text.Encoding (encodeUtf8) -import Web.WebAuthn.Types -import qualified Codec.CBOR.Term as CBOR -import qualified Codec.CBOR.Decoding as CBOR -import qualified Data.Map as Map -import Data.Maybe (fromMaybe) -import qualified Data.ByteString as B -import qualified Data.ByteString.Lazy as BL hiding (pack) -import qualified Data.ByteString.Lazy.Char8 as BL -import qualified Data.ByteArray as BA -import qualified Data.ByteString.Base64 as Base64 -import qualified Data.ByteString.Base64.URL as Base64URL -import qualified Data.X509 as X509 -import qualified Data.X509.Validation as X509 -import qualified Data.X509.CertificateStore as X509 -import Control.Monad.Fail (MonadFail) -import Crypto.Hash (Digest, hash) -import Crypto.Hash.Algorithms (SHA256(..)) -import Control.Monad.IO.Class (MonadIO, liftIO) -import Control.Monad.Trans.Except (ExceptT(..), throwE) -import Data.Char (ord) -import Data.Bifunctor (first) -import Web.WebAuthn.Signature (verifyX509Sig) -import Control.Error.Util (hoistEither, failWith) - -decode :: CBOR.Term -> CBOR.Decoder s StmtSafetyNet -decode (CBOR.TMap xs) = do - let m = Map.fromList xs - let CBOR.TBytes response = fromMaybe (CBOR.TString "response") (Map.lookup (CBOR.TString "response") m) - case (B.split (fromIntegral . ord $ '.') response) of - (h : p : s : _) -> StmtSafetyNet (Base64ByteString h) (Base64ByteString p) (Base64URL.decodeLenient s) <$> getCertificateChain h - _ -> fail ("decodeSafetyNet: response was not a JWT") -decode _ = fail "decodeSafetyNet: expected a Map" - -getCertificateChain :: MonadFail m => ByteString -> m X509.CertificateChain -getCertificateChain h = do - let bs = BL.fromStrict $ Base64URL.decodeLenient h - case (J.eitherDecode bs) of - Left e -> fail ("android-safetynet: Response header decode failed: " <> (show e)) - Right jth -> do - if (alg jth) /= "RS256" then fail ("android-safetynet: Unknown signature alg " <> (show $ alg jth)) else do - let x5cbs = Base64.decodeLenient . encodeUtf8 <$> x5c jth - case X509.decodeCertificateChain (X509.CertificateChainRaw x5cbs) of - Left e -> fail ("Certificate chain decode failed: " <> show e) - Right cc -> pure cc - -verify :: MonadIO m => X509.CertificateStore - -> StmtSafetyNet - -> B.ByteString - -> Digest SHA256 - -> ExceptT VerificationFailure m () -verify cs sf authDataRaw clientDataHash = do - verifyJWS - let dat = authDataRaw <> BA.convert clientDataHash - as <- extractAndroidSafetyNet - let nonceCheck = Base64.encode (BA.convert (hash dat :: Digest SHA256)) - if nonceCheck /= (BL.toStrict $ BL.pack (nonce as)) then throwE NonceCheckFailure else pure () - where - extractAndroidSafetyNet = ExceptT $ pure $ first JSONDecodeError - $ J.eitherDecode (BL.fromStrict . Base64URL.decodeLenient . unBase64ByteString $ payload sf) - verifyJWS = do - let dat = (unBase64ByteString $ header sf) <> "." <> (unBase64ByteString $ payload sf) - res <- liftIO $ X509.validateDefault cs (X509.exceptionValidationCache []) ("attest.android.com", "") (certificates sf) - case res of - [] -> pure () - es -> throwE (MalformedX509Certificate (pack $ show es)) - cert <- failWith MalformedPublicKey (signCert $ certificates sf) - let pub = X509.certPubKey $ X509.getCertificate cert - hoistEither $ verifyX509Sig rs256 pub dat (signature sf) "AndroidSafetyNet" - signCert (X509.CertificateChain cschain) = headMay cschain - -rs256 :: X509.SignatureALG -rs256 = X509.SignatureALG X509.HashSHA256 X509.PubKeyALG_RSA - -headMay :: [a] -> Maybe a -headMay [] = Nothing + {-# LANGUAGE OverloadedStrings #-} +module Web.WebAuthn.AndroidSafetyNet ( + decode, + verify +) where + +import qualified Data.Aeson as J +import Data.ByteString (ByteString) +import Data.Text (pack) +import Data.Text.Encoding (encodeUtf8) +import Web.WebAuthn.Types +import qualified Codec.CBOR.Term as CBOR +import qualified Codec.CBOR.Decoding as CBOR +import qualified Data.Map as Map +import Data.Maybe (fromMaybe) +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as BL hiding (pack) +import qualified Data.ByteString.Lazy.Char8 as BL +import qualified Data.ByteArray as BA +import qualified Data.ByteString.Base64 as Base64 +import qualified Data.ByteString.Base64.URL as Base64URL +import qualified Data.X509 as X509 +import qualified Data.X509.Validation as X509 +import qualified Data.X509.CertificateStore as X509 +import Crypto.Hash (Digest, hash) +import Crypto.Hash.Algorithms (SHA256(..)) +import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Monad.Trans.Except (ExceptT(..), throwE) +import Data.Char (ord) +import Data.Bifunctor (first) +import Web.WebAuthn.Signature (verifyX509Sig) +import Control.Error.Util (hoistEither, failWith) + +decode :: CBOR.Term -> CBOR.Decoder s StmtSafetyNet +decode (CBOR.TMap xs) = do + let m = Map.fromList xs + let CBOR.TBytes response = fromMaybe (CBOR.TString "response") (Map.lookup (CBOR.TString "response") m) + case (B.split (fromIntegral . ord $ '.') response) of + (h : p : s : _) -> StmtSafetyNet (Base64ByteString h) (Base64ByteString p) (Base64URL.decodeLenient s) <$> getCertificateChain h + _ -> fail ("decodeSafetyNet: response was not a JWT") +decode _ = fail "decodeSafetyNet: expected a Map" + +getCertificateChain :: MonadFail m => ByteString -> m X509.CertificateChain +getCertificateChain h = do + let bs = BL.fromStrict $ Base64URL.decodeLenient h + case (J.eitherDecode bs) of + Left e -> fail ("android-safetynet: Response header decode failed: " <> (show e)) + Right jth -> do + if (alg jth) /= "RS256" then fail ("android-safetynet: Unknown signature alg " <> (show $ alg jth)) else do + let x5cbs = Base64.decodeLenient . encodeUtf8 <$> x5c jth + case X509.decodeCertificateChain (X509.CertificateChainRaw x5cbs) of + Left e -> fail ("Certificate chain decode failed: " <> show e) + Right cc -> pure cc + +verify :: MonadIO m => X509.CertificateStore + -> StmtSafetyNet + -> B.ByteString + -> Digest SHA256 + -> ExceptT VerificationFailure m () +verify cs sf authDataRaw clientDataHash = do + verifyJWS + let dat = authDataRaw <> BA.convert clientDataHash + as <- extractAndroidSafetyNet + let nonceCheck = Base64.encode (BA.convert (hash dat :: Digest SHA256)) + if nonceCheck /= (BL.toStrict $ BL.pack (nonce as)) then throwE NonceCheckFailure else pure () + where + extractAndroidSafetyNet = ExceptT $ pure $ first JSONDecodeError + $ J.eitherDecode (BL.fromStrict . Base64URL.decodeLenient . unBase64ByteString $ payload sf) + verifyJWS = do + let dat = (unBase64ByteString $ header sf) <> "." <> (unBase64ByteString $ payload sf) + res <- liftIO $ X509.validateDefault cs (X509.exceptionValidationCache []) ("attest.android.com", "") (certificates sf) + case res of + [] -> pure () + es -> throwE (MalformedX509Certificate (pack $ show es)) + cert <- failWith MalformedPublicKey (signCert $ certificates sf) + let pub = X509.certPubKey $ X509.getCertificate cert + hoistEither $ verifyX509Sig rs256 pub dat (signature sf) "AndroidSafetyNet" + signCert (X509.CertificateChain cschain) = headMay cschain + +rs256 :: X509.SignatureALG +rs256 = X509.SignatureALG X509.HashSHA256 X509.PubKeyALG_RSA + +headMay :: [a] -> Maybe a +headMay [] = Nothing headMay (x : _) = Just x \ No newline at end of file diff --git a/src/Web/WebAuthn/Types.hs b/src/Web/WebAuthn/Types.hs index 61256a3..b355156 100644 --- a/src/Web/WebAuthn/Types.hs +++ b/src/Web/WebAuthn/Types.hs @@ -26,29 +26,58 @@ module Web.WebAuthn.Types ( , StmtSafetyNet(..) , JWTHeader(..) , Base64ByteString(..) + , PublicKeyCredentialRequestOptions(..) + , PublicKeyCredentialDescriptor(..) + , AuthenticatorTransport(..) + , PublicKeyCredentialType(..) ) where import Prelude hiding (fail) import Data.Aeson as J + (Value(..), + (.:), + (.:?), + withObject, + withText, + constructorTagModifier, + FromJSON(..), + ToJSON(..), + Options(..) ) import Data.ByteString (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Base64.URL as Base64 -import Data.ByteString.Base16 as Base16 +import Data.ByteString.Base16 as Base16 (decodeLenient, encode ) import qualified Data.Hashable as H import qualified Data.Map as Map import Data.Text (Text) -import Data.Text.Encoding +import Data.Text.Encoding ( decodeUtf8, encodeUtf8 ) import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Text.Read as T -import Crypto.Hash -import Crypto.Hash.Algorithms (SHA256(..)) +import Crypto.Hash ( SHA256, Digest ) import qualified Codec.CBOR.Term as CBOR import qualified Codec.CBOR.Read as CBOR import qualified Codec.Serialise as CBOR -import Control.Monad.Fail +import Control.Monad.Fail ( MonadFail(fail) ) import GHC.Generics (Generic) import qualified Data.X509 as X509 +import Data.Aeson.Types (typeMismatch) +import Data.Aeson (genericToEncoding) +import Data.Aeson (defaultOptions) +import Data.Char ( toLower ) + +newtype Base64ByteString = Base64ByteString { unBase64ByteString :: ByteString } deriving (Generic, Show, Eq) + +instance ToJSON Base64ByteString where + toJSON (Base64ByteString bs) = String $ decodeUtf8 $ Base64.encode bs + +instance FromJSON Base64ByteString where + parseJSON s@(String v) = do + eth <- pure $ Base64.decode (encodeUtf8 v) + case eth of + Left err -> typeMismatch ("Base64: " <> err) s + Right str -> pure (Base64ByteString str) + parseJSON oth = typeMismatch "Expecting String" oth newtype Challenge = Challenge { rawChallenge :: ByteString } deriving (Show, Eq, Ord, H.Hashable, CBOR.Serialise) @@ -71,7 +100,7 @@ instance FromJSON CollectedClientData where <$> obj .: "type" <*> obj .: "challenge" <*> obj .: "origin" - <*> fmap (maybe TokenBindingUnsupported id) (obj .:? "tokenBinding") + <*> fmap (maybe TokenBindingUnsupported Prelude.id) (obj .:? "tokenBinding") data TokenBinding = TokenBindingUnsupported | TokenBindingSupported @@ -149,7 +178,7 @@ instance ToJSON CredentialPublicKey where newtype AAGUID = AAGUID { unAAGUID :: ByteString } deriving (Show, Eq) instance FromJSON AAGUID where - parseJSON v = AAGUID . fst . Base16.decode . T.encodeUtf8 <$> parseJSON v + parseJSON v = AAGUID . Base16.decodeLenient . T.encodeUtf8 <$> parseJSON v instance ToJSON AAGUID where toJSON = toJSON . T.decodeUtf8 . Base16.encode . unAAGUID @@ -211,8 +240,6 @@ data AndroidSafetyNet = AndroidSafetyNet { instance FromJSON AndroidSafetyNet -newtype Base64ByteString = Base64ByteString {unBase64ByteString :: ByteString} deriving (Show) - data StmtSafetyNet = StmtSafetyNet { header :: Base64ByteString , payload :: Base64ByteString @@ -225,4 +252,38 @@ data JWTHeader = JWTHeader { , x5c :: [Text] } deriving (Show, Generic) -instance FromJSON JWTHeader \ No newline at end of file +instance FromJSON JWTHeader + + +data PublicKeyCredentialType = PublicKey deriving (Eq, Show) + +instance ToJSON PublicKeyCredentialType where + toJSON PublicKey = String "public-key" + +data AuthenticatorTransport = USB -- usb + | NFC -- nfc + | BLE -- ble + | Internal -- internal + deriving (Eq, Show, Generic) + +instance ToJSON AuthenticatorTransport where + toEncoding = genericToEncoding defaultOptions { constructorTagModifier = fmap toLower } + +data PublicKeyCredentialDescriptor = PublicKeyCredentialDescriptor { + tipe :: PublicKeyCredentialType + , id :: Base64ByteString + , transports :: [AuthenticatorTransport] +} deriving (Eq, Show, Generic) + +instance ToJSON PublicKeyCredentialDescriptor where + toEncoding = genericToEncoding defaultOptions { omitNothingFields = True} + +data PublicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions { + challenge :: Base64ByteString + , timeout :: Maybe Integer + , rpId :: Maybe Text + , allowCredentials :: Maybe PublicKeyCredentialDescriptor +} deriving (Eq, Show, Generic) + +instance ToJSON PublicKeyCredentialRequestOptions where + toEncoding = genericToEncoding defaultOptions { omitNothingFields = True} \ No newline at end of file diff --git a/test/Tests.hs b/test/Tests.hs index e497e3d..23b88f3 100644 --- a/test/Tests.hs +++ b/test/Tests.hs @@ -1,63 +1,72 @@ -import Web.WebAuthn -import Test.Tasty -import Test.Tasty.HUnit -import Data.String.Interpolate -import Data.ByteString.Base64.URL as BS (decodeLenient, decode) -import Data.Aeson as A (eitherDecode, FromJSON) -import URI.ByteString -import Data.X509.CertificateStore -import Data.ByteString -import Data.Either -import qualified Data.ByteString.Lazy as BL - -main :: IO () -main = defaultMain tests - -tests :: TestTree -tests = testGroup "Tests" [androidTests] - -androidTests = testGroup "WebAuthn Tests" - [ - androidCredentialTest - ] - -androidCredentialTest = testCaseSteps "Android Test" $ \step -> do - step "Registeration check..." - Just k <- readCertificateStore "test\\cacert.pem" - eth <- registerCredential k androidChallenge defRp Nothing False androidClientDataJSON androidAttestationObject - assertBool (show eth) (isRight eth) - let Right cdata = eth - step "Verification check..." - let eth = verify androidGetChallenge defRp Nothing False androidGetClientDataJSON androidGetAuthenticatorData androidGetSignature (credentialPublicKey cdata) - assertBool (show eth) (isRight eth) - -defRp :: RelyingParty -defRp = defaultRelyingParty $ (Origin "https" "psteniusubi.github.io" Nothing) - -decodePanic :: FromJSON a => ByteString -> a -decodePanic s = either (error) id (A.eitherDecode (BL.fromStrict s)) - -androidClientDataJSON :: ByteString -androidClientDataJSON = BS.decodeLenient "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiWkIyQVJraDZ3RVBoZkdjSFBRWWpWNXNidmxoa3liVlN1ZFQ4Q0VzNTBsNCIsIm9yaWdpbiI6Imh0dHBzOlwvXC9wc3Rlbml1c3ViaS5naXRodWIuaW8iLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uYW5kcm9pZC5jaHJvbWUifQ" - -androidAttestationObject :: ByteString -androidAttestationObject = BS.decodeLenient "o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaTIwMTIxNjAzMGhyZXNwb25zZVkU3mV5SmhiR2NpT2lKU1V6STFOaUlzSW5nMVl5STZXeUpOU1VsR2EzcERRMEpJZFdkQmQwbENRV2RKVWtGT1kxTnJhbVJ6Tlc0MkswTkJRVUZCUVVGd1lUQmpkMFJSV1VwTGIxcEphSFpqVGtGUlJVeENVVUYzVVdwRlRFMUJhMGRCTVZWRlFtaE5RMVpXVFhoSWFrRmpRbWRPVmtKQmIxUkdWV1IyWWpKa2MxcFRRbFZqYmxaNlpFTkNWRnBZU2pKaFYwNXNZM3BGVkUxQ1JVZEJNVlZGUVhoTlMxSXhVbFJKUlU1Q1NVUkdVRTFVUVdWR2R6QjVUVVJCZUUxVVRYaE5WRkY0VGtSc1lVWjNNSGxOVkVGNFRWUkZlRTFVVVhoT1JHeGhUVWQzZUVONlFVcENaMDVXUWtGWlZFRnNWbFJOVWsxM1JWRlpSRlpSVVVsRmQzQkVXVmQ0Y0ZwdE9YbGliV3hvVFZKWmQwWkJXVVJXVVZGSVJYY3hUbUl6Vm5Wa1IwWndZbWxDVjJGWFZqTk5VazEzUlZGWlJGWlJVVXRGZDNCSVlqSTVibUpIVldkVVJYaEVUVkp6ZDBkUldVUldVVkZFUlhoS2FHUklVbXhqTTFGMVdWYzFhMk50T1hCYVF6VnFZakl3ZDJkblJXbE5RVEJIUTFOeFIxTkpZak5FVVVWQ1FWRlZRVUUwU1VKRWQwRjNaMmRGUzBGdlNVSkJVVU5YUlhKQ1VWUkhXa2RPTVdsYVlrNDVaV2hTWjJsbVYwSjRjV2t5VUdSbmVIY3dNMUEzVkhsS1dtWk5lR3B3TlV3M2FqRkhUbVZRU3pWSWVtUnlWVzlKWkRGNVEwbDVRazE1ZUhGbllYcHhaM1J3V0RWWGNITllWelJXWmsxb1NtSk9NVmt3T1hGNmNYQTJTa1FyTWxCYVpHOVVWVEZyUmxKQlRWZG1UQzlWZFZwMGF6ZHdiVkpZWjBkdE5XcExSSEphT1U1NFpUQTBkazFaVVhJNE9FNXhkMWN2YTJaYU1XZFVUMDVKVlZRd1YzTk1WQzgwTlRJeVFsSlhlR1ozZUdNelVVVXhLMVJMVjJ0TVEzSjJaV3MyVjJ4SmNYbGhRelV5VnpkTlJGSTRUWEJHWldKNWJWTkxWSFozWmsxU2QzbExVVXhVTUROVlREUjJkRFE0ZVVWak9ITndOM2RVUVVoTkwxZEVaemhSYjNSaGNtWTRUMEpJYTI1dldqa3lXR2wyYVdGV05uUlJjV2hTVDBoRFptZHRia05ZYVhobVZ6QjNSVmhEZG5GcFRGUmlVWFJWWWt4elV5ODRTVkowWkZocmNGRkNPVUZuVFVKQlFVZHFaMmRLV1UxSlNVTldSRUZQUW1kT1ZraFJPRUpCWmpoRlFrRk5RMEpoUVhkRmQxbEVWbEl3YkVKQmQzZERaMWxKUzNkWlFrSlJWVWhCZDBWM1JFRlpSRlpTTUZSQlVVZ3ZRa0ZKZDBGRVFXUkNaMDVXU0ZFMFJVWm5VVlUyUkVoQ2QzTkJkbUkxTTJjdlF6QTNjSEpVZG5aM1RsRlJURmwzU0hkWlJGWlNNR3BDUW1kM1JtOUJWVzFPU0RSaWFFUnllalYyYzFsS09GbHJRblZuTmpNd1NpOVRjM2RhUVZsSlMzZFpRa0pSVlVoQlVVVkZWMFJDVjAxRFkwZERRM05IUVZGVlJrSjZRVUpvYUhSdlpFaFNkMDlwT0haaU1rNTZZME0xZDJFeWEzVmFNamwyV25rNWJtUklUWGhpZWtWM1MzZFpTVXQzV1VKQ1VWVklUVUZMUjBneWFEQmtTRUUyVEhrNWQyRXlhM1ZhTWpsMlduazVibU16U1hsTU1HUlZWWHBHVUUxVE5XcGpibEYzU0ZGWlJGWlNNRkpDUWxsM1JrbEpVMWxZVWpCYVdFNHdURzFHZFZwSVNuWmhWMUYxV1RJNWRFMURSVWRCTVZWa1NVRlJZVTFDWjNkRFFWbEhXalJGVFVGUlNVTk5RWGRIUTJselIwRlJVVUl4Ym10RFFsRk5kMHgzV1VSV1VqQm1Ra05uZDBwcVFXdHZRMHRuU1VsWlpXRklVakJqUkc5MlRESk9lV0pETlhkaE1tdDFXakk1ZGxwNU9VaFdSazE0VkhwRmRWa3pTbk5OU1VsQ1FrRlpTMHQzV1VKQ1FVaFhaVkZKUlVGblUwSTVVVk5DT0dkRWQwRklZMEU1YkhsVlREbEdNMDFEU1ZWV1FtZEpUVXBTVjJwMVRrNUZlR3Q2ZGprNFRVeDVRVXg2UlRkNFdrOU5RVUZCUm5adWRYa3dXbmRCUVVKQlRVRlRSRUpIUVdsRlFUZGxMekJaVW5VemQwRkdiVmRJTWpkTk1uWmlWbU5hTDIxeWNDczBjbVpaWXk4MVNWQktNamxHTm1kRFNWRkRia3REUTBGaFkxWk9aVmxhT0VORFpsbGtSM0JDTWtkelNIaDFUVTlJYTJFdlR6UXhhbGRsUml0NlowSXhRVVZUVlZwVE5uYzNjeloyZUVWQlNESkxhaXRMVFVSaE5XOUxLekpOYzNoMFZDOVVUVFZoTVhSdlIyOUJRVUZDWWpVM2MzUktUVUZCUVZGRVFVVlpkMUpCU1dkRldHSnBiMUJpU25BNWNVTXdSR295TlRoRVJrZFRVazFCVlN0YVFqRkZhVlpGWW1KaUx6UlZkazVGUTBsQ2FFaHJRblF4T0haU2JqbDZSSFo1Y21aNGVYVmtZMGhVVDFOc00yZFVZVmxCTHpkNVZDOUNhVWcwVFVFd1IwTlRjVWRUU1dJelJGRkZRa04zVlVGQk5FbENRVkZFU1VGalVVSnNiV1E0VFVWblRHUnljbkpOWWtKVVEzWndUVmh6ZERVcmQzZ3lSR3htWVdwS1RrcFZVRFJxV1VacVdWVlJPVUl6V0RSRk1ucG1ORGx1V0ROQmVYVmFSbmhCY1U5U2JtSnFMelZxYTFrM1lUaHhUVW93YWpFNWVrWlBRaXR4WlhKNFpXTXdibWh0T0dkWmJFeGlVVzAyYzB0Wk4xQXdaWGhtY2pkSWRVc3pUV3RRTVhCbFl6RTBkMFpGVldGSGNVUjNWV0pIWjJ3dmIybDZNemhHV0VORkswTlhPRVV4VVVGRlZXWjJZbEZRVkZsaVMzaFphaXQwUTA1c2MzTXdZbFJUYjB3eVdqSmtMMm96UW5CTU0wMUdkekI1ZUZOTEwxVlVjWGxyVEhJeVFTOU5aR2hLVVcxNGFTdEhLMDFMVWxOelVYSTJNa0Z1V21GMU9YRTJXVVp2YVNzNVFVVklLMEUwT0ZoMFNYbHphRXg1UTFSVk0waDBLMkZMYjJoSGJuaEJOWFZzTVZoU2JYRndPRWgyWTBGME16bFFPVFZHV2tkR1NtVXdkWFpzZVdwUGQwRjZXSFZOZFRkTksxQlhVbU1pTENKTlNVbEZVMnBEUTBGNlMyZEJkMGxDUVdkSlRrRmxUekJ0Y1VkT2FYRnRRa3BYYkZGMVJFRk9RbWRyY1docmFVYzVkekJDUVZGelJrRkVRazFOVTBGM1NHZFpSRlpSVVV4RmVHUklZa2M1YVZsWGVGUmhWMlIxU1VaS2RtSXpVV2RSTUVWblRGTkNVMDFxUlZSTlFrVkhRVEZWUlVOb1RVdFNNbmgyV1cxR2MxVXliRzVpYWtWVVRVSkZSMEV4VlVWQmVFMUxVako0ZGxsdFJuTlZNbXh1WW1wQlpVWjNNSGhPZWtFeVRWUlZkMDFFUVhkT1JFcGhSbmN3ZVUxVVJYbE5WRlYzVFVSQmQwNUVTbUZOUlVsNFEzcEJTa0puVGxaQ1FWbFVRV3hXVkUxU05IZElRVmxFVmxGUlMwVjRWa2hpTWpsdVlrZFZaMVpJU2pGak0xRm5WVEpXZVdSdGJHcGFXRTE0UlhwQlVrSm5UbFpDUVUxVVEydGtWVlY1UWtSUlUwRjRWSHBGZDJkblJXbE5RVEJIUTFOeFIxTkpZak5FVVVWQ1FWRlZRVUUwU1VKRWQwRjNaMmRGUzBGdlNVSkJVVVJSUjAwNVJqRkpkazR3TlhwclVVODVLM1JPTVhCSlVuWktlbnA1VDFSSVZ6VkVla1ZhYUVReVpWQkRiblpWUVRCUmF6STRSbWRKUTJaTGNVTTVSV3R6UXpSVU1tWlhRbGxyTDJwRFprTXpVak5XV2sxa1V5OWtUalJhUzBORlVGcFNja0Y2UkhOcFMxVkVlbEp5YlVKQ1NqVjNkV1JuZW01a1NVMVpZMHhsTDFKSFIwWnNOWGxQUkVsTFoycEZkaTlUU2tndlZVd3JaRVZoYkhST01URkNiWE5MSzJWUmJVMUdLeXRCWTNoSFRtaHlOVGx4VFM4NWFXdzNNVWt5WkU0NFJrZG1ZMlJrZDNWaFpXbzBZbGhvY0RCTVkxRkNZbXA0VFdOSk4wcFFNR0ZOTTFRMFNTdEVjMkY0YlV0R2MySnFlbUZVVGtNNWRYcHdSbXhuVDBsbk4zSlNNalY0YjNsdVZYaDJPSFpPYld0eE4zcGtVRWRJV0d0NFYxazNiMGM1YWl0S2ExSjVRa0ZDYXpkWWNrcG1iM1ZqUWxwRmNVWktTbE5RYXpkWVFUQk1TMWN3V1RONk5XOTZNa1F3WXpGMFNrdDNTRUZuVFVKQlFVZHFaMmRGZWsxSlNVSk1la0ZQUW1kT1ZraFJPRUpCWmpoRlFrRk5RMEZaV1hkSVVWbEVWbEl3YkVKQ1dYZEdRVmxKUzNkWlFrSlJWVWhCZDBWSFEwTnpSMEZSVlVaQ2QwMURUVUpKUjBFeFZXUkZkMFZDTDNkUlNVMUJXVUpCWmpoRFFWRkJkMGhSV1VSV1VqQlBRa0paUlVaS2FsSXJSelJSTmpncllqZEhRMlpIU2tGaWIwOTBPVU5tTUhKTlFqaEhRVEZWWkVsM1VWbE5RbUZCUmtwMmFVSXhaRzVJUWpkQllXZGlaVmRpVTJGTVpDOWpSMWxaZFUxRVZVZERRM05IUVZGVlJrSjNSVUpDUTJ0M1NucEJiRUpuWjNKQ1owVkdRbEZqZDBGWldWcGhTRkl3WTBSdmRrd3lPV3BqTTBGMVkwZDBjRXh0WkhaaU1tTjJXak5PZVUxcVFYbENaMDVXU0ZJNFJVdDZRWEJOUTJWblNtRkJhbWhwUm05a1NGSjNUMms0ZGxrelNuTk1ia0p5WVZNMWJtSXlPVzVNTW1SNlkycEpkbG96VG5sTmFUVnFZMjEzZDFCM1dVUldVakJuUWtSbmQwNXFRVEJDWjFwdVoxRjNRa0ZuU1hkTGFrRnZRbWRuY2tKblJVWkNVV05EUVZKWlkyRklVakJqU0UwMlRIazVkMkV5YTNWYU1qbDJXbms1ZVZwWVFuWmpNbXd3WWpOS05VeDZRVTVDWjJ0eGFHdHBSemwzTUVKQlVYTkdRVUZQUTBGUlJVRkhiMEVyVG01dU56aDVObkJTYW1RNVdHeFJWMDVoTjBoVVoybGFMM0l6VWs1SGEyMVZiVmxJVUZGeE5sTmpkR2s1VUVWaGFuWjNVbFF5YVZkVVNGRnlNREptWlhOeFQzRkNXVEpGVkZWM1oxcFJLMnhzZEc5T1JuWm9jMDg1ZEhaQ1EwOUpZWHB3YzNkWFF6bGhTamw0YW5VMGRGZEVVVWc0VGxaVk5sbGFXaTlZZEdWRVUwZFZPVmw2U25GUWFsazRjVE5OUkhoeWVtMXhaWEJDUTJZMWJ6aHRkeTkzU2pSaE1rYzJlSHBWY2paR1lqWlVPRTFqUkU4eU1sQk1Va3cyZFROTk5GUjZjek5CTWsweGFqWmllV3RLV1drNGQxZEpVbVJCZGt0TVYxcDFMMkY0UWxaaWVsbHRjVzEzYTIwMWVreFRSRmMxYmtsQlNtSkZURU5SUTFwM1RVZzFOblF5UkhaeGIyWjRjelpDUW1ORFJrbGFWVk53ZUhVMmVEWjBaREJXTjFOMlNrTkRiM05wY2xOdFNXRjBhaTg1WkZOVFZrUlJhV0psZERoeEx6ZFZTelIyTkZwVlRqZ3dZWFJ1V25veGVXYzlQU0pkZlEuZXlKdWIyNWpaU0k2SW5KS1lXcExhM1pEUm01aE0yUlpXVzVVWTFSQ1FWRnNlbkE1WVhVemMwWXpZVzVxTjBaVWJFbHpSRlU5SWl3aWRHbHRaWE4wWVcxd1RYTWlPakUxT0RnM05UazFNRFEyTkRFc0ltRndhMUJoWTJ0aFoyVk9ZVzFsSWpvaVkyOXRMbWR2YjJkc1pTNWhibVJ5YjJsa0xtZHRjeUlzSW1Gd2EwUnBaMlZ6ZEZOb1lUSTFOaUk2SWtGMmJTOU1MMmxHU1hkcmNuaE5TakJJU1V4M2NqVjRTa2xoVFZWUlREWlFjMGhFWWtWa2NVMXJja0U5SWl3aVkzUnpVSEp2Wm1sc1pVMWhkR05vSWpwMGNuVmxMQ0poY0d0RFpYSjBhV1pwWTJGMFpVUnBaMlZ6ZEZOb1lUSTFOaUk2V3lJNFVERnpWekJGVUVwamMyeDNOMVY2VW5OcFdFdzJOSGNyVHpVd1JXUXJVa0pKUTNSaGVURm5NalJOUFNKZExDSmlZWE5wWTBsdWRHVm5jbWwwZVNJNmRISjFaU3dpWlhaaGJIVmhkR2x2YmxSNWNHVWlPaUpDUVZOSlF5SjkuWXZtN1ZGNmVpeUhYWEMyanprdjJ2QTdQNGRYd3NobkxvYlN1Q2NHbEtYRFkzeFhLVkxlUTdWalZ6QkpyU1J2ODROYlh0TzFqanZ6WVdQLTNJcDdEWktXc2dBeEpJSk1SeHhwQU44UUJiWUlPS2Yzamxxczd4VWtMM2pNdVl2bFVsbkNseUJuaEpvTm9tN3JWZE04SmdiajMtUVQxRGhSNUt0WEVUbV9HaFFEanJrdHBJd201N3RGRFYwOHRVVEtrTkpmNkNnNDV3Y0plbnJ2UlZTUXBseXh1cVY4al91QWl5SkxGdTV5dk1qZ0o3WkdkLXRZX1ZscS1zNXQ2NTVSTnYtaHNFQTZhdTdyTzNJYjFQQVh3X0xGVENveXdKLVhVd0xqRkpqZTdieGpnQUx2SWtrOE5BUGpXYXh2YWcyRzMyNGs4RWdjSzc3U0dxNHhES1Zfek5BaGF1dGhEYXRhWMUs15PPoLQYy78OqFIihgfZ6XszPU2wpBAXdmr2u4x1UUUAAAAAuT_ZYfLmRi-xIoIAIkfeeABBAQJBVPhy4yG7tNUTkedMIgadvfK55s6r3qX_V5jaBOfycETIQLr7zGs_GrMbXGrkJU2BTCDU_uuea4WwBffTv_GlAQIDJiABIVggca4oTyEumIkH8am4WBD7h90D_SSj6cRf7ksf3HhbefoiWCD6gxvdhHuqvBsamD01kD6pCiVWakup0S0BNRYj0U7hOg" - -androidChallenge :: Challenge -androidChallenge = Challenge (BS.decodeLenient "ZB2ARkh6wEPhfGcHPQYjV5sbvlhkybVSudT8CEs50l4") - -androidGetChallenge :: Challenge -androidGetChallenge = Challenge (BS.decodeLenient "dCCcJkllvbdd-LKDJrCQYbouMEY3FEsNljYis_temyA") - --- This contains the Get Challenge in it -androidGetClientDataJSON :: ByteString -androidGetClientDataJSON = BS.decodeLenient "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZENDY0prbGx2YmRkLUxLREpyQ1FZYm91TUVZM0ZFc05sallpc190ZW15QSIsIm9yaWdpbiI6Imh0dHBzOlwvXC9wc3Rlbml1c3ViaS5naXRodWIuaW8iLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uYW5kcm9pZC5jaHJvbWUifQ" - -androidGetAuthenticatorData :: ByteString -androidGetAuthenticatorData = BS.decodeLenient "LNeTz6C0GMu_DqhSIoYH2el7Mz1NsKQQF3Zq9ruMdVEFAAAAAQ" - -androidGetSignature :: ByteString -androidGetSignature = BS.decodeLenient "MEQCIFM6aZjT8CefzdAn-QNaa5OcPU24V1SERVocZlus1YT1AiAH_UqNj7xVOW1sDLKkpicTxIONpwfWrWNbo8KL4z5wcA" - -errorOnLeft (Left e) = error e +import Web.WebAuthn + ( CredentialData(credentialPublicKey), + RelyingParty, + Origin(Origin), + Challenge(Challenge), + defaultRelyingParty, + registerCredential, + verify ) +import Test.Tasty ( defaultMain, testGroup, TestTree ) +import Test.Tasty.HUnit ( assertBool, testCaseSteps ) +import Data.String.Interpolate () +import Data.ByteString.Base64.URL as BS (decodeLenient) +import Data.Aeson as A (eitherDecode, FromJSON) +import URI.ByteString () +import Data.X509.CertificateStore ( readCertificateStore ) +import Data.ByteString ( ByteString ) +import Data.Either ( isRight ) +import qualified Data.ByteString.Lazy as BL + +main :: IO () +main = defaultMain tests + +tests :: TestTree +tests = testGroup "Tests" [androidTests] + +androidTests :: TestTree +androidTests = testGroup "WebAuthn Tests" + [ + androidCredentialTest + ] + +androidCredentialTest :: TestTree +androidCredentialTest = testCaseSteps "Android Test" $ \step -> do + step "Registeration check..." + Just k <- readCertificateStore "test/cacert.pem" + eth <- registerCredential k androidChallenge defRp Nothing False androidClientDataJSON androidAttestationObject + assertBool (show eth) (isRight eth) + let Right cdata = eth + step "Verification check..." + let eth = verify androidGetChallenge defRp Nothing False androidGetClientDataJSON androidGetAuthenticatorData androidGetSignature (credentialPublicKey cdata) + assertBool (show eth) (isRight eth) + +defRp :: RelyingParty +defRp = defaultRelyingParty $ (Origin "https" "psteniusubi.github.io" Nothing) + +decodePanic :: FromJSON a => ByteString -> a +decodePanic s = either (error) id (A.eitherDecode (BL.fromStrict s)) + +androidClientDataJSON :: ByteString +androidClientDataJSON = BS.decodeLenient "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiWkIyQVJraDZ3RVBoZkdjSFBRWWpWNXNidmxoa3liVlN1ZFQ4Q0VzNTBsNCIsIm9yaWdpbiI6Imh0dHBzOlwvXC9wc3Rlbml1c3ViaS5naXRodWIuaW8iLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uYW5kcm9pZC5jaHJvbWUifQ" + +androidAttestationObject :: ByteString +androidAttestationObject = BS.decodeLenient "o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaTIwMTIxNjAzMGhyZXNwb25zZVkU3mV5SmhiR2NpT2lKU1V6STFOaUlzSW5nMVl5STZXeUpOU1VsR2EzcERRMEpJZFdkQmQwbENRV2RKVWtGT1kxTnJhbVJ6Tlc0MkswTkJRVUZCUVVGd1lUQmpkMFJSV1VwTGIxcEphSFpqVGtGUlJVeENVVUYzVVdwRlRFMUJhMGRCTVZWRlFtaE5RMVpXVFhoSWFrRmpRbWRPVmtKQmIxUkdWV1IyWWpKa2MxcFRRbFZqYmxaNlpFTkNWRnBZU2pKaFYwNXNZM3BGVkUxQ1JVZEJNVlZGUVhoTlMxSXhVbFJKUlU1Q1NVUkdVRTFVUVdWR2R6QjVUVVJCZUUxVVRYaE5WRkY0VGtSc1lVWjNNSGxOVkVGNFRWUkZlRTFVVVhoT1JHeGhUVWQzZUVONlFVcENaMDVXUWtGWlZFRnNWbFJOVWsxM1JWRlpSRlpSVVVsRmQzQkVXVmQ0Y0ZwdE9YbGliV3hvVFZKWmQwWkJXVVJXVVZGSVJYY3hUbUl6Vm5Wa1IwWndZbWxDVjJGWFZqTk5VazEzUlZGWlJGWlJVVXRGZDNCSVlqSTVibUpIVldkVVJYaEVUVkp6ZDBkUldVUldVVkZFUlhoS2FHUklVbXhqTTFGMVdWYzFhMk50T1hCYVF6VnFZakl3ZDJkblJXbE5RVEJIUTFOeFIxTkpZak5FVVVWQ1FWRlZRVUUwU1VKRWQwRjNaMmRGUzBGdlNVSkJVVU5YUlhKQ1VWUkhXa2RPTVdsYVlrNDVaV2hTWjJsbVYwSjRjV2t5VUdSbmVIY3dNMUEzVkhsS1dtWk5lR3B3TlV3M2FqRkhUbVZRU3pWSWVtUnlWVzlKWkRGNVEwbDVRazE1ZUhGbllYcHhaM1J3V0RWWGNITllWelJXWmsxb1NtSk9NVmt3T1hGNmNYQTJTa1FyTWxCYVpHOVVWVEZyUmxKQlRWZG1UQzlWZFZwMGF6ZHdiVkpZWjBkdE5XcExSSEphT1U1NFpUQTBkazFaVVhJNE9FNXhkMWN2YTJaYU1XZFVUMDVKVlZRd1YzTk1WQzgwTlRJeVFsSlhlR1ozZUdNelVVVXhLMVJMVjJ0TVEzSjJaV3MyVjJ4SmNYbGhRelV5VnpkTlJGSTRUWEJHWldKNWJWTkxWSFozWmsxU2QzbExVVXhVTUROVlREUjJkRFE0ZVVWak9ITndOM2RVUVVoTkwxZEVaemhSYjNSaGNtWTRUMEpJYTI1dldqa3lXR2wyYVdGV05uUlJjV2hTVDBoRFptZHRia05ZYVhobVZ6QjNSVmhEZG5GcFRGUmlVWFJWWWt4elV5ODRTVkowWkZocmNGRkNPVUZuVFVKQlFVZHFaMmRLV1UxSlNVTldSRUZQUW1kT1ZraFJPRUpCWmpoRlFrRk5RMEpoUVhkRmQxbEVWbEl3YkVKQmQzZERaMWxKUzNkWlFrSlJWVWhCZDBWM1JFRlpSRlpTTUZSQlVVZ3ZRa0ZKZDBGRVFXUkNaMDVXU0ZFMFJVWm5VVlUyUkVoQ2QzTkJkbUkxTTJjdlF6QTNjSEpVZG5aM1RsRlJURmwzU0hkWlJGWlNNR3BDUW1kM1JtOUJWVzFPU0RSaWFFUnllalYyYzFsS09GbHJRblZuTmpNd1NpOVRjM2RhUVZsSlMzZFpRa0pSVlVoQlVVVkZWMFJDVjAxRFkwZERRM05IUVZGVlJrSjZRVUpvYUhSdlpFaFNkMDlwT0haaU1rNTZZME0xZDJFeWEzVmFNamwyV25rNWJtUklUWGhpZWtWM1MzZFpTVXQzV1VKQ1VWVklUVUZMUjBneWFEQmtTRUUyVEhrNWQyRXlhM1ZhTWpsMlduazVibU16U1hsTU1HUlZWWHBHVUUxVE5XcGpibEYzU0ZGWlJGWlNNRkpDUWxsM1JrbEpVMWxZVWpCYVdFNHdURzFHZFZwSVNuWmhWMUYxV1RJNWRFMURSVWRCTVZWa1NVRlJZVTFDWjNkRFFWbEhXalJGVFVGUlNVTk5RWGRIUTJselIwRlJVVUl4Ym10RFFsRk5kMHgzV1VSV1VqQm1Ra05uZDBwcVFXdHZRMHRuU1VsWlpXRklVakJqUkc5MlRESk9lV0pETlhkaE1tdDFXakk1ZGxwNU9VaFdSazE0VkhwRmRWa3pTbk5OU1VsQ1FrRlpTMHQzV1VKQ1FVaFhaVkZKUlVGblUwSTVVVk5DT0dkRWQwRklZMEU1YkhsVlREbEdNMDFEU1ZWV1FtZEpUVXBTVjJwMVRrNUZlR3Q2ZGprNFRVeDVRVXg2UlRkNFdrOU5RVUZCUm5adWRYa3dXbmRCUVVKQlRVRlRSRUpIUVdsRlFUZGxMekJaVW5VemQwRkdiVmRJTWpkTk1uWmlWbU5hTDIxeWNDczBjbVpaWXk4MVNWQktNamxHTm1kRFNWRkRia3REUTBGaFkxWk9aVmxhT0VORFpsbGtSM0JDTWtkelNIaDFUVTlJYTJFdlR6UXhhbGRsUml0NlowSXhRVVZUVlZwVE5uYzNjeloyZUVWQlNESkxhaXRMVFVSaE5XOUxLekpOYzNoMFZDOVVUVFZoTVhSdlIyOUJRVUZDWWpVM2MzUktUVUZCUVZGRVFVVlpkMUpCU1dkRldHSnBiMUJpU25BNWNVTXdSR295TlRoRVJrZFRVazFCVlN0YVFqRkZhVlpGWW1KaUx6UlZkazVGUTBsQ2FFaHJRblF4T0haU2JqbDZSSFo1Y21aNGVYVmtZMGhVVDFOc00yZFVZVmxCTHpkNVZDOUNhVWcwVFVFd1IwTlRjVWRUU1dJelJGRkZRa04zVlVGQk5FbENRVkZFU1VGalVVSnNiV1E0VFVWblRHUnljbkpOWWtKVVEzWndUVmh6ZERVcmQzZ3lSR3htWVdwS1RrcFZVRFJxV1VacVdWVlJPVUl6V0RSRk1ucG1ORGx1V0ROQmVYVmFSbmhCY1U5U2JtSnFMelZxYTFrM1lUaHhUVW93YWpFNWVrWlBRaXR4WlhKNFpXTXdibWh0T0dkWmJFeGlVVzAyYzB0Wk4xQXdaWGhtY2pkSWRVc3pUV3RRTVhCbFl6RTBkMFpGVldGSGNVUjNWV0pIWjJ3dmIybDZNemhHV0VORkswTlhPRVV4VVVGRlZXWjJZbEZRVkZsaVMzaFphaXQwUTA1c2MzTXdZbFJUYjB3eVdqSmtMMm96UW5CTU0wMUdkekI1ZUZOTEwxVlVjWGxyVEhJeVFTOU5aR2hLVVcxNGFTdEhLMDFMVWxOelVYSTJNa0Z1V21GMU9YRTJXVVp2YVNzNVFVVklLMEUwT0ZoMFNYbHphRXg1UTFSVk0waDBLMkZMYjJoSGJuaEJOWFZzTVZoU2JYRndPRWgyWTBGME16bFFPVFZHV2tkR1NtVXdkWFpzZVdwUGQwRjZXSFZOZFRkTksxQlhVbU1pTENKTlNVbEZVMnBEUTBGNlMyZEJkMGxDUVdkSlRrRmxUekJ0Y1VkT2FYRnRRa3BYYkZGMVJFRk9RbWRyY1docmFVYzVkekJDUVZGelJrRkVRazFOVTBGM1NHZFpSRlpSVVV4RmVHUklZa2M1YVZsWGVGUmhWMlIxU1VaS2RtSXpVV2RSTUVWblRGTkNVMDFxUlZSTlFrVkhRVEZWUlVOb1RVdFNNbmgyV1cxR2MxVXliRzVpYWtWVVRVSkZSMEV4VlVWQmVFMUxVako0ZGxsdFJuTlZNbXh1WW1wQlpVWjNNSGhPZWtFeVRWUlZkMDFFUVhkT1JFcGhSbmN3ZVUxVVJYbE5WRlYzVFVSQmQwNUVTbUZOUlVsNFEzcEJTa0puVGxaQ1FWbFVRV3hXVkUxU05IZElRVmxFVmxGUlMwVjRWa2hpTWpsdVlrZFZaMVpJU2pGak0xRm5WVEpXZVdSdGJHcGFXRTE0UlhwQlVrSm5UbFpDUVUxVVEydGtWVlY1UWtSUlUwRjRWSHBGZDJkblJXbE5RVEJIUTFOeFIxTkpZak5FVVVWQ1FWRlZRVUUwU1VKRWQwRjNaMmRGUzBGdlNVSkJVVVJSUjAwNVJqRkpkazR3TlhwclVVODVLM1JPTVhCSlVuWktlbnA1VDFSSVZ6VkVla1ZhYUVReVpWQkRiblpWUVRCUmF6STRSbWRKUTJaTGNVTTVSV3R6UXpSVU1tWlhRbGxyTDJwRFprTXpVak5XV2sxa1V5OWtUalJhUzBORlVGcFNja0Y2UkhOcFMxVkVlbEp5YlVKQ1NqVjNkV1JuZW01a1NVMVpZMHhsTDFKSFIwWnNOWGxQUkVsTFoycEZkaTlUU2tndlZVd3JaRVZoYkhST01URkNiWE5MSzJWUmJVMUdLeXRCWTNoSFRtaHlOVGx4VFM4NWFXdzNNVWt5WkU0NFJrZG1ZMlJrZDNWaFpXbzBZbGhvY0RCTVkxRkNZbXA0VFdOSk4wcFFNR0ZOTTFRMFNTdEVjMkY0YlV0R2MySnFlbUZVVGtNNWRYcHdSbXhuVDBsbk4zSlNNalY0YjNsdVZYaDJPSFpPYld0eE4zcGtVRWRJV0d0NFYxazNiMGM1YWl0S2ExSjVRa0ZDYXpkWWNrcG1iM1ZqUWxwRmNVWktTbE5RYXpkWVFUQk1TMWN3V1RONk5XOTZNa1F3WXpGMFNrdDNTRUZuVFVKQlFVZHFaMmRGZWsxSlNVSk1la0ZQUW1kT1ZraFJPRUpCWmpoRlFrRk5RMEZaV1hkSVVWbEVWbEl3YkVKQ1dYZEdRVmxKUzNkWlFrSlJWVWhCZDBWSFEwTnpSMEZSVlVaQ2QwMURUVUpKUjBFeFZXUkZkMFZDTDNkUlNVMUJXVUpCWmpoRFFWRkJkMGhSV1VSV1VqQlBRa0paUlVaS2FsSXJSelJSTmpncllqZEhRMlpIU2tGaWIwOTBPVU5tTUhKTlFqaEhRVEZWWkVsM1VWbE5RbUZCUmtwMmFVSXhaRzVJUWpkQllXZGlaVmRpVTJGTVpDOWpSMWxaZFUxRVZVZERRM05IUVZGVlJrSjNSVUpDUTJ0M1NucEJiRUpuWjNKQ1owVkdRbEZqZDBGWldWcGhTRkl3WTBSdmRrd3lPV3BqTTBGMVkwZDBjRXh0WkhaaU1tTjJXak5PZVUxcVFYbENaMDVXU0ZJNFJVdDZRWEJOUTJWblNtRkJhbWhwUm05a1NGSjNUMms0ZGxrelNuTk1ia0p5WVZNMWJtSXlPVzVNTW1SNlkycEpkbG96VG5sTmFUVnFZMjEzZDFCM1dVUldVakJuUWtSbmQwNXFRVEJDWjFwdVoxRjNRa0ZuU1hkTGFrRnZRbWRuY2tKblJVWkNVV05EUVZKWlkyRklVakJqU0UwMlRIazVkMkV5YTNWYU1qbDJXbms1ZVZwWVFuWmpNbXd3WWpOS05VeDZRVTVDWjJ0eGFHdHBSemwzTUVKQlVYTkdRVUZQUTBGUlJVRkhiMEVyVG01dU56aDVObkJTYW1RNVdHeFJWMDVoTjBoVVoybGFMM0l6VWs1SGEyMVZiVmxJVUZGeE5sTmpkR2s1VUVWaGFuWjNVbFF5YVZkVVNGRnlNREptWlhOeFQzRkNXVEpGVkZWM1oxcFJLMnhzZEc5T1JuWm9jMDg1ZEhaQ1EwOUpZWHB3YzNkWFF6bGhTamw0YW5VMGRGZEVVVWc0VGxaVk5sbGFXaTlZZEdWRVUwZFZPVmw2U25GUWFsazRjVE5OUkhoeWVtMXhaWEJDUTJZMWJ6aHRkeTkzU2pSaE1rYzJlSHBWY2paR1lqWlVPRTFqUkU4eU1sQk1Va3cyZFROTk5GUjZjek5CTWsweGFqWmllV3RLV1drNGQxZEpVbVJCZGt0TVYxcDFMMkY0UWxaaWVsbHRjVzEzYTIwMWVreFRSRmMxYmtsQlNtSkZURU5SUTFwM1RVZzFOblF5UkhaeGIyWjRjelpDUW1ORFJrbGFWVk53ZUhVMmVEWjBaREJXTjFOMlNrTkRiM05wY2xOdFNXRjBhaTg1WkZOVFZrUlJhV0psZERoeEx6ZFZTelIyTkZwVlRqZ3dZWFJ1V25veGVXYzlQU0pkZlEuZXlKdWIyNWpaU0k2SW5KS1lXcExhM1pEUm01aE0yUlpXVzVVWTFSQ1FWRnNlbkE1WVhVemMwWXpZVzVxTjBaVWJFbHpSRlU5SWl3aWRHbHRaWE4wWVcxd1RYTWlPakUxT0RnM05UazFNRFEyTkRFc0ltRndhMUJoWTJ0aFoyVk9ZVzFsSWpvaVkyOXRMbWR2YjJkc1pTNWhibVJ5YjJsa0xtZHRjeUlzSW1Gd2EwUnBaMlZ6ZEZOb1lUSTFOaUk2SWtGMmJTOU1MMmxHU1hkcmNuaE5TakJJU1V4M2NqVjRTa2xoVFZWUlREWlFjMGhFWWtWa2NVMXJja0U5SWl3aVkzUnpVSEp2Wm1sc1pVMWhkR05vSWpwMGNuVmxMQ0poY0d0RFpYSjBhV1pwWTJGMFpVUnBaMlZ6ZEZOb1lUSTFOaUk2V3lJNFVERnpWekJGVUVwamMyeDNOMVY2VW5OcFdFdzJOSGNyVHpVd1JXUXJVa0pKUTNSaGVURm5NalJOUFNKZExDSmlZWE5wWTBsdWRHVm5jbWwwZVNJNmRISjFaU3dpWlhaaGJIVmhkR2x2YmxSNWNHVWlPaUpDUVZOSlF5SjkuWXZtN1ZGNmVpeUhYWEMyanprdjJ2QTdQNGRYd3NobkxvYlN1Q2NHbEtYRFkzeFhLVkxlUTdWalZ6QkpyU1J2ODROYlh0TzFqanZ6WVdQLTNJcDdEWktXc2dBeEpJSk1SeHhwQU44UUJiWUlPS2Yzamxxczd4VWtMM2pNdVl2bFVsbkNseUJuaEpvTm9tN3JWZE04SmdiajMtUVQxRGhSNUt0WEVUbV9HaFFEanJrdHBJd201N3RGRFYwOHRVVEtrTkpmNkNnNDV3Y0plbnJ2UlZTUXBseXh1cVY4al91QWl5SkxGdTV5dk1qZ0o3WkdkLXRZX1ZscS1zNXQ2NTVSTnYtaHNFQTZhdTdyTzNJYjFQQVh3X0xGVENveXdKLVhVd0xqRkpqZTdieGpnQUx2SWtrOE5BUGpXYXh2YWcyRzMyNGs4RWdjSzc3U0dxNHhES1Zfek5BaGF1dGhEYXRhWMUs15PPoLQYy78OqFIihgfZ6XszPU2wpBAXdmr2u4x1UUUAAAAAuT_ZYfLmRi-xIoIAIkfeeABBAQJBVPhy4yG7tNUTkedMIgadvfK55s6r3qX_V5jaBOfycETIQLr7zGs_GrMbXGrkJU2BTCDU_uuea4WwBffTv_GlAQIDJiABIVggca4oTyEumIkH8am4WBD7h90D_SSj6cRf7ksf3HhbefoiWCD6gxvdhHuqvBsamD01kD6pCiVWakup0S0BNRYj0U7hOg" + +androidChallenge :: Challenge +androidChallenge = Challenge (BS.decodeLenient "ZB2ARkh6wEPhfGcHPQYjV5sbvlhkybVSudT8CEs50l4") + +androidGetChallenge :: Challenge +androidGetChallenge = Challenge (BS.decodeLenient "dCCcJkllvbdd-LKDJrCQYbouMEY3FEsNljYis_temyA") + +-- This contains the Get Challenge in it +androidGetClientDataJSON :: ByteString +androidGetClientDataJSON = BS.decodeLenient "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZENDY0prbGx2YmRkLUxLREpyQ1FZYm91TUVZM0ZFc05sallpc190ZW15QSIsIm9yaWdpbiI6Imh0dHBzOlwvXC9wc3Rlbml1c3ViaS5naXRodWIuaW8iLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uYW5kcm9pZC5jaHJvbWUifQ" + +androidGetAuthenticatorData :: ByteString +androidGetAuthenticatorData = BS.decodeLenient "LNeTz6C0GMu_DqhSIoYH2el7Mz1NsKQQF3Zq9ruMdVEFAAAAAQ" + +androidGetSignature :: ByteString +androidGetSignature = BS.decodeLenient "MEQCIFM6aZjT8CefzdAn-QNaa5OcPU24V1SERVocZlus1YT1AiAH_UqNj7xVOW1sDLKkpicTxIONpwfWrWNbo8KL4z5wcA" + +errorOnLeft (Left e) = error e errorOnLeft (Right r) = r \ No newline at end of file diff --git a/webauthn.cabal b/webauthn.cabal index f5db48a..54ad2c4 100644 --- a/webauthn.cabal +++ b/webauthn.cabal @@ -50,11 +50,14 @@ library , x509-validation , memory , serialise - , base16-bytestring + , base16-bytestring >= 1.0.0.0 && < 1.1.0.0 , base64-bytestring hs-source-dirs: src ghc-options: -Wall default-language: Haskell2010 + default-extensions: + DuplicateRecordFields + OverloadedStrings test-suite test-webauthn import: base-common