module Crypto.JOSE.JWS.Internal where
import Control.Applicative
import Data.Char
import Data.Maybe
import Data.Aeson
import Data.Aeson.Parser
import Data.Aeson.Types
import qualified Data.Attoparsec.ByteString.Lazy as A
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Base64.URL.Lazy as B64UL
import qualified Data.HashMap.Strict as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as E
import Data.Traversable (sequenceA)
import qualified Data.Vector as V
import Crypto.JOSE.Classes
import Crypto.JOSE.Compact
import qualified Crypto.JOSE.JWA.JWS as JWA.JWS
import Crypto.JOSE.JWK
import qualified Crypto.JOSE.Types as Types
import qualified Crypto.JOSE.Types.Internal as Types
critInvalidNames :: [T.Text]
critInvalidNames = [
"alg"
, "jku"
, "jwk"
, "x5u"
, "x5t"
, "x5c"
, "kid"
, "typ"
, "cty"
, "crit"
]
data CritParameters = CritParameters (M.HashMap T.Text Value)
deriving (Eq, Show)
critObjectParser :: M.HashMap T.Text Value -> Value -> Parser (T.Text, Value)
critObjectParser o (String s)
| s `elem` critInvalidNames = fail "crit key is reserved"
| otherwise = (\v -> (s, v)) <$> o .: s
critObjectParser _ _ = fail "crit key is not text"
instance FromJSON CritParameters where
parseJSON = withObject "crit" (\o ->
case M.lookup "crit" o of
Just (Array paramNames)
| V.null paramNames -> fail "crit cannot be empty"
| otherwise -> fmap (CritParameters . M.fromList)
$ sequenceA
$ map (critObjectParser o)
$ V.toList paramNames
_ -> fail "crit is not an array")
instance ToJSON CritParameters where
toJSON (CritParameters m) = Object $ M.insert "crit" (toJSON $ M.keys m) m
data Header = Header
{ headerAlg :: JWA.JWS.Alg
, headerJku :: Maybe Types.URI
, headerJwk :: Maybe JWK
, headerKid :: Maybe String
, headerX5u :: Maybe Types.URI
, headerX5t :: Maybe Types.Base64SHA1
, headerX5c :: Maybe [Types.Base64X509]
, headerTyp :: Maybe String
, headerCty :: Maybe String
, headerCrit :: Maybe CritParameters
, headerRaw :: Maybe BS.ByteString
}
deriving (Show)
instance Eq Header where
a == b =
let
ignoreRaw (Header alg jku jwk kid x5u x5t x5c typ cty crit _)
= (alg, jku, jwk, kid, x5u, x5t, x5c, typ, cty, crit)
in
ignoreRaw a == ignoreRaw b
parseHeaderWith
:: (forall a. FromJSON a => T.Text -> Parser a)
-> (forall a. FromJSON a => T.Text -> Parser (Maybe a))
-> Parser (Maybe CritParameters)
-> Parser Header
parseHeaderWith req opt crit = Header
<$> req "alg"
<*> opt "jku"
<*> opt "jwk"
<*> opt "kid"
<*> opt "x5u"
<*> opt "x5t"
<*> opt "x5c"
<*> opt "typ"
<*> opt "cty"
<*> crit
<*> pure Nothing
parseCrit :: Object -> Parser (Maybe CritParameters)
parseCrit o = if M.member "crit" o
then Just <$> parseJSON (Object o)
else pure Nothing
instance FromJSON Header where
parseJSON = withObject "JWS Header" (\o ->
parseHeaderWith (o .:) (o .:?) (parseCrit o))
instance ToJSON Header where
toJSON (Header alg jku jwk kid x5u x5t x5c typ cty crit _) = object $ catMaybes [
Just ("alg" .= alg)
, fmap ("jku" .=) jku
, fmap ("jwk" .=) jwk
, fmap ("kid" .=) kid
, fmap ("x5u" .=) x5u
, fmap ("x5t" .=) x5t
, fmap ("x5c" .=) x5c
, fmap ("typ" .=) typ
, fmap ("cty" .=) cty
]
++ Types.objectPairs (toJSON crit)
algHeader :: JWA.JWS.Alg -> Header
algHeader alg = Header alg n n n n n n n n n n where n = Nothing
(.::) :: (FromJSON a) => Object -> Object -> T.Text -> Parser a
(.::) o1 o2 k = case (M.lookup k o1, M.lookup k o2) of
(Just _, Just _) -> fail $ "key " ++ show k ++ " cannot appear twice"
(Just v, _) -> parseJSON v
(_, Just v) -> parseJSON v
_ -> fail $ "key " ++ show k ++ " now present"
(.::?) :: (FromJSON a) => Object -> Object -> T.Text -> Parser (Maybe a)
(.::?) o1 o2 k = case (M.lookup k o1, M.lookup k o2) of
(Just _, Just _) -> fail $ "key " ++ show k ++ " cannot appear twice"
(Just v, _) -> parseJSON v
(_, Just v) -> parseJSON v
_ -> pure Nothing
data Signature = Signature Header Types.Base64Octets
deriving (Eq, Show)
parseHeader :: Maybe Object -> Maybe Object -> Parser Header
parseHeader (Just p) (Just u) = parseHeaderWith ((.::) p u) ((.::?) p u) (parseCrit p)
parseHeader (Just p) _ = parseHeaderWith (p .:) (p .:?) (parseCrit p)
parseHeader _ (Just u) = parseHeaderWith (u .:) (u .:?) (if M.member "crit" u
then fail "crit MUST occur only with the JWS Protected Header"
else pure Nothing)
parseHeader _ _ = fail "no protected or unprotected header given"
newtype JSONByteString = JSONByteString { bs :: BS.ByteString }
instance FromJSON JSONByteString where
parseJSON = withText "JSON bytestring" (return . JSONByteString . E.encodeUtf8)
instance FromJSON Signature where
parseJSON = withObject "signature" (\o -> Signature
<$> do
protectedEncoded <- o .:? "protected"
protectedJSON <- maybe
(pure Nothing)
(withText "base64 encoded header"
(Types.parseB64Url (return . Just . JSONByteString)))
protectedEncoded
protected <- maybe
(pure Nothing)
(return . decode . BSL.fromStrict)
(fmap bs protectedJSON)
unprotected <- o .:? "header"
parseHeader protected unprotected
<*> o .: "signature")
instance ToJSON Signature where
toJSON (Signature h s) = object $ ("signature" .= s) : Types.objectPairs (toJSON h)
data JWS = JWS Types.Base64Octets [Signature]
deriving (Eq, Show)
instance FromJSON JWS where
parseJSON = withObject "JWS JSON serialization" (\o -> JWS
<$> o .: "payload"
<*> o .: "signatures")
instance ToJSON JWS where
toJSON (JWS p ss) = object ["payload" .= p, "signatures" .= ss]
jwsPayload :: JWS -> BSL.ByteString
jwsPayload (JWS (Types.Base64Octets s) _) = BSL.fromStrict s
encodeO :: ToJSON a => a -> BSL.ByteString
encodeO = BSL.reverse . BSL.dropWhile (== c) . BSL.reverse
. B64UL.encode . encode
where c = fromIntegral $ ord '='
decodeO :: FromJSON a => BSL.ByteString -> Either String a
decodeO s = B64UL.decode (pad s) >>= eitherDecode
where
pad t = t `BSL.append` BSL.replicate ((4 BSL.length t `mod` 4) `mod` 4) c
c = fromIntegral $ ord '='
encodeS :: ToJSON a => a -> BSL.ByteString
encodeS = BSL.init . BSL.tail . encode
decodeS :: FromJSON a => BSL.ByteString -> Maybe a
decodeS s = do
v <- A.maybeResult $ A.parse value $ BSL.intercalate s ["\"", "\""]
parseMaybe parseJSON v
signingInput :: Header -> Types.Base64Octets -> BSL.ByteString
signingInput h p = BSL.intercalate "."
[maybe (encodeO h) BSL.fromStrict (headerRaw h), encodeS p]
instance ToCompact JWS where
toCompact (JWS p [Signature h s]) = Right [signingInput h p, encodeS s]
toCompact (JWS _ xs) =
Left $ "cannot compact serialize JWS with " ++ show (length xs) ++ " sigs"
instance FromCompact JWS where
fromCompact [h, p, s] = do
h' <- (\h' -> h' { headerRaw = Just $ BSL.toStrict h }) <$> decodeO h
p' <- maybe (Left "payload decode failed") Right $ decodeS p
s' <- maybe (Left "sig decode failed") Right $ decodeS s
return $ JWS p' [Signature h' s']
fromCompact xs = Left $ "expected 3 parts, got " ++ show (length xs)
signJWS
:: JWS
-> Header
-> JWK
-> JWS
signJWS (JWS p sigs) h k = JWS p (sig:sigs) where
sig = Signature h $ Types.Base64Octets $
sign (headerAlg h) k (BSL.toStrict $ signingInput h p)
verifyJWS :: JWK -> JWS -> Bool
verifyJWS k (JWS p sigs) = any (verifySig k p) sigs
verifySig :: JWK -> Types.Base64Octets -> Signature -> Bool
verifySig k m (Signature h (Types.Base64Octets s))
= verify (headerAlg h) k (BSL.toStrict $ signingInput h m) s