u2f-0.1.0.2: src/U2F.hs
{-# LANGUAGE DeriveGeneric #-}
{- |
__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
-}
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.Aeson (decode)
import Data.Binary.Get
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 qualified Crypto.Hash.SHA256 as SHA256
-- Cryptonite stuff
import Crypto.Error
import Crypto.PubKey.ECC.Types
import qualified Crypto.PubKey.ECC.P256 as P256
import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
import Crypto.Hash.Algorithms
-- | The U2F Spec (currently) exclusively supports use of the SEC p256r Curve
ourCurve :: Curve
ourCurve = getCurveByName SEC_p256r1
-- | 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)
getPubKeyFromCertificate :: BS.ByteString -> Either U2FError ECDSA.PublicKey
getPubKeyFromCertificate cert = case (decodeASN1' DER cert) of
Right certParse -> case (findPubKey certParse) of
Just key -> Right key
Nothing -> Left PubKeyParsingError
Left _ -> Left RegistrationCertificateParseError
findPubKey :: Foldable t => t ASN1 -> Maybe ECDSA.PublicKey
findPubKey parsedCert = case (find pubKeyShape parsedCert) of
-- Eventually check to make sure this is not compressed, in right format
Just (BitString (BitArray _ x)) -> parsePublicKey $ BS.tail x
_ -> Nothing
pubKeyShape :: ASN1 -> Bool
pubKeyShape (BitString (BitArray len _)) = len == 520
pubKeyShape _ = False
formatSignatureBase :: BS.ByteString -> BS.ByteString -> BS.ByteString -> BS.ByteString -> BS.ByteString
formatSignatureBase _appId clientData _keyHandle publicKey = sigBase
where sigBase = BS.concat([BS.pack "\NUL", SHA256.hash(_appId), SHA256.hash(decodeLenient clientData), _keyHandle, publicKey])
getSignatureBaseFromRegistration :: Registration -> RegistrationData -> BS.ByteString
getSignatureBaseFromRegistration registration registrationData = formatSignatureBase aId clientData kH publicKey
where aId = encodeUtf8 $ registration_appId registration
clientData = encodeUtf8 $ registration_clientData registration
kH = registrationData_keyHandle registrationData
publicKey = registrationData_publicKey registrationData
-- | Verifies that Registration is a valid response to the Request
verifyRegistration :: Request -> Registration -> Either U2FError Request
verifyRegistration request registration = do
_ <- u2fComparator (challenge request) (registration_challenge registration) ChallengeMismatchError
registrationData <- parseRegistrationData $ encodeUtf8 $ registration_registrationData registration
pkey <- getPubKeyFromCertificate $ registrationData_certificate registrationData
signature <- parseSignature $ registrationData_signature registrationData
let signatureBase = getSignatureBaseFromRegistration registration registrationData
case (verifySignature signatureBase pkey signature) of
True -> Right (request {keyHandle = Just $ formatOutputBase64 $ registrationData_keyHandle registrationData})
False -> Left FailedVerificationError
-- | 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.
-- Warning!: Expects uncompressed public key.
verifySignin :: BS.ByteString -> Request -> Signin -> Either U2FError Bool
verifySignin savedPubkey request signin = do
clientData <- parseClientData $ encodeUtf8 $ signin_clientData signin
_ <- u2fComparator (challenge request) (clientData_challenge clientData) ChallengeMismatchError
signatureData <- parseSignatureData $ encodeUtf8 $ signin_signatureData signin
signature <- parseSignature $ signatureData_signature signatureData
let signatureBase = getSigninSignatureBase request signin signatureData
-- TODO: write function that checks first byte for compression state, parses each pubkey format
-- Technically, only uncompressed keys are allowed by the FIDO U2F spec, but it's possible that
-- a compressed key could be added to user data via other means.
publicKey <- case (parsePublicKey $ BS.tail $ savedPubkey) of
Just key -> Right key
Nothing -> Left PubKeyParsingError
case (verifySignature signatureBase publicKey signature) of
True -> Right True
False -> Left FailedVerificationError
parseSignatureData :: BS.ByteString -> Either U2FError SignatureData
parseSignatureData s = Right $ runGet unpackSignatureData ( LBS.fromStrict $ decodeLenient s)
parseSignature :: BS.ByteString -> Either U2FError ECDSA.Signature
parseSignature possibleSig = case (decodeASN1' DER possibleSig) of
Right ([_, IntVal r, IntVal s, _]) -> Right $ ECDSA.Signature r s
_ -> Left SignatureParseError
getSigninSignatureBase :: Request -> Signin -> SignatureData -> BS.ByteString
getSigninSignatureBase request signin signatureData = BS.concat([SHA256.hash(aId), userPresenceFlag, counter, SHA256.hash(decodeLenient clientData)])
where aId = encodeUtf8 $ appId request
userPresenceFlag = signatureData_userPresenceFlag signatureData
counter = signatureData_counter signatureData
clientData = encodeUtf8 $ signin_clientData signin
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 _ -> Nothing
-- | 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)
verifySignature :: BS.ByteString -> ECDSA.PublicKey -> ECDSA.Signature -> Bool
verifySignature sigBase pubKey signature = ECDSA.verify Crypto.Hash.Algorithms.SHA256 pubKey signature sigBase
u2fComparator :: (Eq a) => a -> a -> U2FError -> Either U2FError Bool
u2fComparator firstThing secondThing theError = case (firstThing == secondThing) of
True -> Right True
False -> Left theError
unpackRegistrationData :: Get RegistrationData
unpackRegistrationData = do
reserved <- getByteString 1
publicKey <- getByteString 65
keyHandleLen <- getWord8
kH <- getByteString $ fromIntegral keyHandleLen
cert <- unpackASN1
sign <- unpackASN1
return $ RegistrationData reserved publicKey kH cert sign
unpackSignatureData :: Get SignatureData
unpackSignatureData = do
userPresenceFlag <- getByteString 1
counter <- getByteString 4
signature <- unpackASN1
return $ SignatureData userPresenceFlag counter signature
unpackASN1 :: Get BS.ByteString
unpackASN1 = do
asnPadding <- getWord8
asnLen <- getWord8
if ((.&.) asnLen 128) /= 0
then do
firstByte <- getWord8
secondByte <- getWord8
let firstLen = (fromIntegral firstByte :: Int)
let secondLen = (fromIntegral secondByte :: Int)
let asnLength = (firstLen * 256) + secondLen
asnBody <- getByteString asnLength
return $ BS.concat([pack([asnPadding, asnLen, firstByte, secondByte]), asnBody])
else do
asnBody <- getByteString (fromIntegral asnLen)
return $ BS.concat([pack([asnPadding, asnLen]), asnBody])