packages feed

jose 0.2.33.0 → 0.2.38.0

raw patch · 11 files changed

+333/−163 lines, 11 filesdep +bifunctorsdep +data-default-classdep +lensdep −networkPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: bifunctors, data-default-class, lens, network-uri, semigroups

Dependencies removed: network

API changes (from Hackage documentation)

- Crypto.JOSE.JWS: headerRaw :: JWSHeader -> Maybe ByteString
- Crypto.JWT: IntDate :: UTCTime -> IntDate
- Crypto.JWT: instance Eq IntDate
- Crypto.JWT: instance FromJSON IntDate
- Crypto.JWT: instance Show IntDate
- Crypto.JWT: instance ToJSON IntDate
- Crypto.JWT: newtype IntDate
+ Crypto.JOSE.Error: JWSCritUnprotected :: Error
+ Crypto.JOSE.Error: JWSDuplicateHeaderParameter :: Error
+ Crypto.JOSE.Error: JWSMissingAlg :: Error
+ Crypto.JOSE.Error: JWSMissingHeader :: Error
+ Crypto.JOSE.Error: OtherPrimesNotSupported :: Error
+ Crypto.JOSE.JWS: AllValidated :: ValidationPolicy
+ Crypto.JOSE.JWS: AnyValidated :: ValidationPolicy
+ Crypto.JOSE.JWS: ValidationAlgorithms :: [Alg] -> ValidationAlgorithms
+ Crypto.JOSE.JWS: data ValidationPolicy
+ Crypto.JOSE.JWS: newtype ValidationAlgorithms
+ Crypto.JWT: NumericDate :: UTCTime -> NumericDate
+ Crypto.JWT: instance Eq NumericDate
+ Crypto.JWT: instance FromJSON NumericDate
+ Crypto.JWT: instance Show NumericDate
+ Crypto.JWT: instance ToJSON NumericDate
+ Crypto.JWT: newtype NumericDate
- Crypto.JOSE.JWS: JWSHeader :: Alg -> Maybe URI -> Maybe JWK -> Maybe String -> Maybe URI -> Maybe [Base64X509] -> Maybe Base64SHA1 -> Maybe Base64SHA256 -> Maybe String -> Maybe String -> Maybe CritParameters -> Maybe ByteString -> JWSHeader
+ Crypto.JOSE.JWS: JWSHeader :: Maybe Alg -> Maybe URI -> Maybe JWK -> Maybe String -> Maybe URI -> Maybe (NonEmpty Base64X509) -> Maybe Base64SHA1 -> Maybe Base64SHA256 -> Maybe String -> Maybe String -> Maybe CritParameters -> JWSHeader
- Crypto.JOSE.JWS: headerAlg :: JWSHeader -> Alg
+ Crypto.JOSE.JWS: headerAlg :: JWSHeader -> Maybe Alg
- Crypto.JOSE.JWS: headerX5c :: JWSHeader -> Maybe [Base64X509]
+ Crypto.JOSE.JWS: headerX5c :: JWSHeader -> Maybe (NonEmpty Base64X509)
- Crypto.JOSE.JWS: verifyJWS :: JWK -> JWS -> Bool
+ Crypto.JOSE.JWS: verifyJWS :: ValidationAlgorithms -> ValidationPolicy -> JWK -> JWS -> Bool
- Crypto.JWT: ClaimsSet :: Maybe StringOrURI -> Maybe StringOrURI -> Maybe Audience -> Maybe IntDate -> Maybe IntDate -> Maybe IntDate -> Maybe Text -> HashMap Text Value -> ClaimsSet
+ Crypto.JWT: ClaimsSet :: Maybe StringOrURI -> Maybe StringOrURI -> Maybe Audience -> Maybe NumericDate -> Maybe NumericDate -> Maybe NumericDate -> Maybe Text -> HashMap Text Value -> ClaimsSet
- Crypto.JWT: claimExp :: ClaimsSet -> Maybe IntDate
+ Crypto.JWT: claimExp :: ClaimsSet -> Maybe NumericDate
- Crypto.JWT: claimIat :: ClaimsSet -> Maybe IntDate
+ Crypto.JWT: claimIat :: ClaimsSet -> Maybe NumericDate
- Crypto.JWT: claimNbf :: ClaimsSet -> Maybe IntDate
+ Crypto.JWT: claimNbf :: ClaimsSet -> Maybe NumericDate
- Crypto.JWT: validateJWSJWT :: JWK -> JWT -> Bool
+ Crypto.JWT: validateJWSJWT :: ValidationAlgorithms -> ValidationPolicy -> JWK -> JWT -> Bool

Files

jose.cabal view
@@ -1,5 +1,5 @@ name:                jose-version:             0.2.33.0+version:             0.2.38.0 synopsis:   Javascript Object Signing and Encryption and JSON Web Token library description:@@ -50,24 +50,30 @@     Crypto.JOSE.JWA.JWE.Alg     Crypto.JOSE.JWS.Internal     Crypto.JOSE.TH+    Crypto.JOSE.Types.Armour     Crypto.JOSE.Types.Internal+    Crypto.JOSE.Types.Orphans    build-depends:     base == 4.*     , attoparsec     , base64-bytestring == 1.0.*+    , bifunctors >= 4.0     , byteable == 0.1.*     , crypto-pubkey >= 0.2.3     , crypto-pubkey-types >= 0.3.2     , crypto-random >= 0.0.7 && < 0.0.9     , cryptohash == 0.11.*+    , data-default-class+    , lens >= 4.3     , template-haskell >= 2.4+    , semigroups >= 0.15     , aeson >= 0.7 && < 0.9     , unordered-containers == 0.2.*     , bytestring == 0.10.*     , text == 1.1.*     , time == 1.4.*-    , network >= 2.4+    , network-uri >= 2.6     , certificate == 1.3.*     , vector @@ -87,19 +93,23 @@     base     , attoparsec     , base64-bytestring+    , bifunctors     , byteable     , crypto-pubkey     , crypto-pubkey-types     , crypto-random     , cryptohash+    , data-default-class+    , lens     , old-locale     , template-haskell+    , semigroups     , aeson     , unordered-containers     , bytestring     , text     , time-    , network+    , network-uri     , certificate     , vector     , hspec
src/Crypto/JOSE/Error.hs view
@@ -34,8 +34,13 @@   | AlgorithmMismatch String  -- ^ A requested algorithm cannot be used   | KeyMismatch String        -- ^ Wrong type of key was given   | KeySizeTooSmall           -- ^ Key size is too small+  | OtherPrimesNotSupported   -- ^ RSA private key with >2 primes not supported   | RSAError RSA.Error        -- ^ RSA encryption, decryption or signing error   | CompactEncodeError String -- ^ Cannot produce compact representation of data   | CompactDecodeError String -- ^ Cannot decode compact representation   | JSONDecodeError String    -- ^ Cannot decode JSON data+  | JWSMissingHeader+  | JWSMissingAlg+  | JWSCritUnprotected+  | JWSDuplicateHeaderParameter   deriving (Eq, Show)
src/Crypto/JOSE/JWA/JWK.hs view
@@ -46,7 +46,7 @@   ) where  import Control.Applicative-import Control.Arrow+import Data.Bifunctor import Data.Maybe  import Crypto.Hash@@ -61,6 +61,7 @@ import Data.Byteable import qualified Data.ByteString as B import qualified Data.HashMap.Strict as M+import Data.List.NonEmpty  import Crypto.JOSE.Error import Crypto.JOSE.Classes@@ -110,7 +111,7 @@   , rsaDp :: Types.Base64Integer   , rsaDq :: Types.Base64Integer   , rsaQi :: Types.Base64Integer-  , rsaOth :: Maybe [RSAPrivateKeyOthElem] -- TODO oth must not be empty array+  , rsaOth :: Maybe (NonEmpty RSAPrivateKeyOthElem)   }   deriving (Eq, Show) @@ -226,7 +227,7 @@   where   pubkey = ECDSA.PublicKey (curve k) (point k)   sig = uncurry ECDSA.Signature-    $ Types.bsToInteger *** Types.bsToInteger+    $ bimap Types.bsToInteger Types.bsToInteger     $ B.splitAt (B.length s `div` 2) s  curve :: ECKeyParameters -> ECC.Curve@@ -314,7 +315,7 @@   -> (Either Error B.ByteString, g) signPKCS15 h k g m = case rsaPrivateKey k of   Left e -> (Left e, g)-  Right k' -> first (either (Left . RSAError) Right) $+  Right k' -> first (first RSAError) $     PKCS15.signSafer g h k' m  verifyPKCS15@@ -334,7 +335,7 @@   -> (Either Error B.ByteString, g) signPSS h k g m = case rsaPrivateKey k of   Left e -> (Left e, g)-  Right k' -> first (either (Left . RSAError) Right) $+  Right k' -> first (first RSAError) $    PSS.signSafer g (PSS.defaultPSSParams (hashFunction h)) k' m  verifyPSS@@ -350,10 +351,16 @@ rsaPrivateKey (RSAKeyParameters _   (Types.SizedBase64Integer size n)   (Types.Base64Integer e)-  (Just (RSAPrivateKeyParameters (Types.Base64Integer d) _)))-  | size >= 2048 `div` 8 = Right $-    RSA.PrivateKey (RSA.PublicKey size n e) d 0 0 0 0 0-  | otherwise = Left KeySizeTooSmall+  (Just (RSAPrivateKeyParameters (Types.Base64Integer d) opt)))+  | isJust (opt >>= rsaOth) = Left OtherPrimesNotSupported+  | size < 2048 `div` 8 = Left KeySizeTooSmall+  | otherwise = Right $+    RSA.PrivateKey (RSA.PublicKey size n e) d+      (opt' rsaP) (opt' rsaQ) (opt' rsaDp) (opt' rsaDq) (opt' rsaQi)+    where+      opt' f = fromMaybe 0 (unB64I . f <$> opt)+      unB64I (Types.Base64Integer x) = x+ rsaPrivateKey _ = Left $ KeyMismatch "not an RSA private key"  rsaPublicKey :: RSAKeyParameters -> RSA.PublicKey
src/Crypto/JOSE/JWK.hs view
@@ -35,7 +35,7 @@   ) where  import Control.Applicative-import Control.Arrow+import Data.Bifunctor import Data.Maybe (catMaybes)  import Data.Aeson
src/Crypto/JOSE/JWS.hs view
@@ -1,4 +1,4 @@--- Copyright (C) 2013  Fraser Tweedale+-- Copyright (C) 2013, 2014  Fraser Tweedale -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License.@@ -26,6 +26,9 @@   , JWS(..)   , jwsPayload   , signJWS++  , ValidationAlgorithms(..)+  , ValidationPolicy(..)   , verifyJWS   ) where 
src/Crypto/JOSE/JWS/Internal.hs view
@@ -12,19 +12,23 @@ -- See the License for the specific language governing permissions and -- limitations under the License. +{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE Rank2Types #-} {-# OPTIONS_HADDOCK hide #-}  module Crypto.JOSE.JWS.Internal where +import Prelude hiding (mapM)+ import Control.Applicative-import Control.Arrow+import Control.Monad ((>=>), when, unless)+import Data.Bifunctor import Data.Char import Data.Maybe +import Control.Lens ((^.)) import Data.Aeson-import Data.Aeson.Parser+import qualified Data.Aeson.Parser as P import Data.Aeson.Types import qualified Data.Attoparsec.ByteString.Lazy as A import Data.Byteable@@ -32,11 +36,12 @@ import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Base64.URL as B64U import qualified Data.ByteString.Base64.URL.Lazy as B64UL-import qualified Data.HashMap.Strict as M+import Data.Default.Class+import Data.HashMap.Strict (member)+import Data.List.NonEmpty (NonEmpty(..), toList) import qualified Data.Text as T-import qualified Data.Text.Encoding as E-import Data.Traversable (sequenceA)-import qualified Data.Vector as V+import qualified Data.Text.Encoding as T+import Data.Traversable (mapM)  import Crypto.JOSE.Classes import Crypto.JOSE.Compact@@ -45,6 +50,7 @@ import Crypto.JOSE.JWK import qualified Crypto.JOSE.Types as Types import qualified Crypto.JOSE.Types.Internal as Types+import Crypto.JOSE.Types.Armour   critInvalidNames :: [T.Text]@@ -62,86 +68,70 @@   , "crit"   ] -data CritParameters = CritParameters (M.HashMap T.Text Value)+newtype CritParameters = CritParameters (NonEmpty (T.Text, Value))   deriving (Eq, Show) -critObjectParser :: M.HashMap T.Text Value -> Value -> Parser (T.Text, Value)-critObjectParser o (String s)+critObjectParser :: Object -> T.Text -> Parser (T.Text, Value)+critObjectParser o s   | s `elem` critInvalidNames = fail "crit key is reserved"   | otherwise                 = (\v -> (s, v)) <$> o .: s-critObjectParser _ _          = fail "crit key is not text" --- TODO implement array length >= 1 restriction+parseCrit :: Object -> NonEmpty T.Text -> Parser CritParameters+parseCrit o = fmap CritParameters . mapM (critObjectParser o)+  -- TODO fail on duplicate strings+ 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")+  parseJSON = withObject "crit" $ \o -> o .: "crit" >>= parseCrit o  instance ToJSON CritParameters where-  toJSON (CritParameters m) = Object $ M.insert "crit" (toJSON $ M.keys m) m+  toJSON (CritParameters m) = object $ ("crit", toJSON $ fmap fst m) : toList m   -- | JWS Header data type. data JWSHeader = JWSHeader-  { headerAlg :: JWA.JWS.Alg+  { headerAlg :: Maybe JWA.JWS.Alg   , headerJku :: Maybe Types.URI  -- ^ JWK Set URL   , headerJwk :: Maybe JWK   , headerKid :: Maybe String  -- ^ interpretation unspecified   , headerX5u :: Maybe Types.URI-  , headerX5c :: Maybe [Types.Base64X509] -- ^ TODO implement min len of 1+  , headerX5c :: Maybe (NonEmpty Types.Base64X509)   , headerX5t :: Maybe Types.Base64SHA1   , headerX5tS256 :: Maybe Types.Base64SHA256   , headerTyp :: Maybe String  -- ^ Content Type (of object)   , headerCty :: Maybe String  -- ^ Content Type (of payload)   , headerCrit :: Maybe CritParameters-  , headerRaw :: Maybe BS.ByteString  -- ^ protected header, if known   }-  deriving (Show)--instance Eq JWSHeader where-  a == b =-    let-      ignoreRaw (JWSHeader alg jku jwk kid x5u x5c x5t x5tS256 typ cty crit _)-        = (alg, jku, jwk, kid, x5u, x5c, x5t, x5tS256, typ, cty, crit)-    in-      ignoreRaw a == ignoreRaw b+  deriving (Eq, Show) -parseHeaderWith-  :: (forall a. FromJSON a => T.Text -> Parser a)-  -> (forall a. FromJSON a => T.Text -> Parser (Maybe a))-  -> Parser (Maybe CritParameters)-  -> Parser JWSHeader-parseHeaderWith req opt crit = JWSHeader-    <$> req "alg"-    <*> opt "jku"-    <*> opt "jwk"-    <*> opt "kid"-    <*> opt "x5u"-    <*> opt "x5t"-    <*> opt "x5t#S256"-    <*> opt "x5c"-    <*> opt "typ"-    <*> opt "cty"-    <*> crit-    <*> pure Nothing+instance FromArmour T.Text Error JWSHeader where+  parseArmour s =+        first (compactErr "header")+          (B64UL.decode (pad $ BSL.fromStrict $ T.encodeUtf8 s))+        >>= first JSONDecodeError . eitherDecode+    where+    compactErr s' = CompactDecodeError . ((s' ++ " decode failed: ") ++)+    pad t = t `BSL.append` BSL.replicate ((4 - BSL.length t `mod` 4) `mod` 4) c+    c = fromIntegral $ ord '=' -parseCrit :: Object -> Parser (Maybe CritParameters)-parseCrit o = if M.member "crit" o-  then Just <$> parseJSON (Object o)-  else pure Nothing+instance ToArmour T.Text JWSHeader where+  toArmour = T.decodeUtf8 . Types.unpad . B64U.encode . BSL.toStrict . encode  instance FromJSON JWSHeader where-  parseJSON = withObject "JWS Header" (\o ->-    parseHeaderWith (o .:) (o .:?) (parseCrit o))+  parseJSON = withObject "JWS Header" $ \o -> JWSHeader+    <$> o .:? "alg"+    <*> o .:? "jku"+    <*> o .:? "jwk"+    <*> o .:? "kid"+    <*> o .:? "x5u"+    <*> o .:? "x5t"+    <*> o .:? "x5t#S256"+    <*> o .:? "x5c"+    <*> o .:? "typ"+    <*> o .:? "cty"+    <*> (o .:? "crit" >>= mapM (parseCrit o))  instance ToJSON JWSHeader where-  toJSON (JWSHeader alg jku jwk kid x5u x5c x5t x5tS256 typ cty crit _) =+  toJSON (JWSHeader alg jku jwk kid x5u x5c x5t x5tS256 typ cty crit) =     object $ catMaybes       [ Just ("alg" .= alg)       , fmap ("jku" .=) jku@@ -155,63 +145,52 @@       , fmap ("cty" .=) cty       ] ++ Types.objectPairs (toJSON crit) +instance Default JWSHeader where+  def = JWSHeader z z z z z z z z z z z where z = Nothing  -- construct a minimal header with the given alg algHeader :: JWA.JWS.Alg -> JWSHeader-algHeader alg = JWSHeader alg n 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+algHeader alg = def { headerAlg = Just alg }  -data Signature = Signature JWSHeader Types.Base64Octets+data Signature = Signature+  (Maybe (Armour T.Text JWSHeader))+  (Maybe JWSHeader)+  Types.Base64Octets   deriving (Eq, Show) -parseHeader :: Maybe Object -> Maybe Object -> Parser JWSHeader-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 }+algorithm :: Signature -> Maybe JWA.JWS.Alg+algorithm (Signature h h' _) = (h >>= headerAlg . (^. value)) <|> (h' >>= headerAlg) -instance FromJSON JSONByteString where-  parseJSON = withText "JSON bytestring" (return . JSONByteString . E.encodeUtf8)+checkHeaders :: Signature -> Either Error Signature+checkHeaders sig@(Signature h h' _) = do+  unless (isJust h || isJust h') (Left JWSMissingHeader)+  unless (isJust $ algorithm sig) (Left JWSMissingAlg)+  when (isJust $ h' >>= headerCrit) (Left JWSCritUnprotected)+  when hasDup (Left JWSDuplicateHeaderParameter)+  return sig+  where+    isDup f = isJust (h >>= f . (^. value)) && isJust (h' >>= f)+    hasDup = or+      [ isDup headerAlg, isDup headerJku, isDup headerJwk+      , isDup headerKid, isDup headerX5u, isDup headerX5c+      , isDup headerX5t, isDup headerX5tS256, isDup headerTyp+      , isDup headerCty+      ]  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")+  parseJSON =+    withObject "signature" (\o -> Signature+      <$> o .:? "protected"+      <*> o .:? "header"+      <*> o .: "signature"+    ) >=> either (fail . show) pure . checkHeaders  instance ToJSON Signature where-  toJSON (Signature h s) = object $ ("signature" .= s) : Types.objectPairs (toJSON h)+  toJSON (Signature h h' s) =+    object $ ("signature" .= s) :+      maybe [] (Types.objectPairs . toJSON . (^. value)) h+      ++ maybe [] (Types.objectPairs . toJSON) h'   -- | JSON Web Signature data type.  Consists of a payload and a@@ -221,10 +200,16 @@   deriving (Eq, Show)  instance FromJSON JWS where-  parseJSON = withObject "JWS JSON serialization" (\o -> JWS-    <$> o .: "payload"-    <*> o .: "signatures")+  parseJSON v =+    withObject "JWS JSON serialization" (\o -> JWS+      <$> o .: "payload"+      <*> o .: "signatures") v+    <|> withObject "Flattened JWS JSON serialization" (\o ->+      if member "signatures" o+      then fail "\"signatures\" member MUST NOT be present"+      else (\p s -> JWS p [s]) <$> o .: "payload" <*> parseJSON v) v + instance ToJSON JWS where   toJSON (JWS p ss) = object ["payload" .= p, "signatures" .= ss] @@ -233,12 +218,9 @@ jwsPayload :: JWS -> BSL.ByteString jwsPayload (JWS (Types.Base64Octets s) _) = BSL.fromStrict s --signingInput :: JWSHeader -> Types.Base64Octets -> BS.ByteString+signingInput :: Maybe (Armour T.Text JWSHeader) -> Types.Base64Octets -> BS.ByteString signingInput h p = BS.intercalate "."-  [ fromMaybe-    (Types.unpad $ B64U.encode $ BSL.toStrict $ encode h)-    (headerRaw h)+  [ maybe "" (T.encodeUtf8 . (^. armour)) h   , toBytes p   ] @@ -248,7 +230,7 @@ -- signature and returns Nothing otherwise -- instance ToCompact JWS where-  toCompact (JWS p [Signature h s]) =+  toCompact (JWS p [Signature h _ s]) =     Right [BSL.fromStrict $ signingInput h p, BSL.fromStrict $ toBytes s]   toCompact (JWS _ xs) = Left $ CompactEncodeError $     "cannot compact serialize JWS with " ++ show (length xs) ++ " sigs"@@ -256,25 +238,18 @@ instance FromCompact JWS where   fromCompact xs = case xs of     [h, p, s] -> do-      h' <- (\h' -> h' { headerRaw = Just $ BSL.toStrict h })-        <$> decodeO "header" h+      h' <- decodeArmour $ T.decodeUtf8 $ BSL.toStrict h       p' <- decodeS "payload" p       s' <- decodeS "signature" s-      return $ JWS p' [Signature h' s']-    xs' -> compactErr "compact representation"+      return $ JWS p' [Signature (Just h') Nothing s']+    xs' -> Left $ compactErr "compact representation"       $ "expected 3 parts, got " ++ show (length xs')     where-      compactErr s = Left . CompactDecodeError . ((s ++ " decode failed: ") ++)-      jsonErr = Left . JSONDecodeError-      decodeO desc s =-        either (compactErr desc) Right (B64UL.decode (pad s))-        >>= either jsonErr Right . eitherDecode+      compactErr s = CompactDecodeError . ((s ++ " decode failed: ") ++)       decodeS desc s =-        either (compactErr desc) Right-          (A.eitherResult $ A.parse value $ BSL.intercalate s ["\"", "\""])-        >>= either jsonErr Right . parseEither parseJSON-      pad t = t `BSL.append` BSL.replicate ((4 - BSL.length t `mod` 4) `mod` 4) c-      c = fromIntegral $ ord '='+        first (compactErr desc)+          (A.eitherResult $ A.parse P.value $ BSL.intercalate s ["\"", "\""])+        >>= first JSONDecodeError . parseEither parseJSON   -- §5.1. Message Signing or MACing@@ -288,19 +263,66 @@   -> JWSHeader  -- ^ Header for signature   -> JWK      -- ^ Key with which to sign   -> (Either Error JWS, g) -- ^ JWS with new signature appended-signJWS g (JWS p sigs) h k = first (either Left (Right . appendSig)) $-  sign (headerAlg h) k g (signingInput h p)+signJWS g (JWS p sigs) h k = first (second appendSig) $ case headerAlg h of+    Nothing   -> (Left JWSMissingAlg, g)+    Just alg  -> sign alg k g (signingInput h' p)   where-    appendSig sig = JWS p (Signature h (Types.Base64Octets sig):sigs)+    appendSig sig = JWS p (Signature h' Nothing (Types.Base64Octets sig):sigs)+    h' = Just $ Unarmoured h ++-- | Algorithms for which validation will be attempted.  The default+-- value includes all algorithms except 'None'.+--+newtype ValidationAlgorithms = ValidationAlgorithms [JWA.JWS.Alg]++instance Default ValidationAlgorithms where+  def = ValidationAlgorithms+    [ JWA.JWS.HS256, JWA.JWS.HS384, JWA.JWS.HS512+    , JWA.JWS.RS256, JWA.JWS.RS384, JWA.JWS.RS512+    , JWA.JWS.ES256, JWA.JWS.ES384, JWA.JWS.ES512+    , JWA.JWS.PS256, JWA.JWS.PS384, JWA.JWS.PS512+    ]++-- | Validation policy.  The default policy is 'AllValidated'.+--+data ValidationPolicy+  = AnyValidated+  -- ^ One successfully validated signature is sufficient+  | AllValidated+  -- ^ All signatures for which validation is attempted must be validated++instance Default ValidationPolicy where+  def = AllValidated++ -- | Verify a JWS. -- -- Verification succeeds if any signature on the JWS is successfully -- validated with the given 'Key'. ---verifyJWS :: JWK -> JWS -> Bool-verifyJWS k (JWS p sigs) = any ((== Right True) . verifySig k p) sigs+-- If only specific signatures need to be validated, and the+-- 'ValidationPolicy' argument is not enough to express this,+-- the caller is responsible for removing irrelevant signatures+-- prior to calling 'verifyJWS'.+--+verifyJWS+  :: ValidationAlgorithms+  -> ValidationPolicy+  -> JWK+  -> JWS+  -> Bool+verifyJWS (ValidationAlgorithms algs) policy k (JWS p sigs) =+  applyPolicy policy $ map validate $ filter shouldValidateSig sigs+  where+  shouldValidateSig = maybe False (`elem` algs) . algorithm+  applyPolicy AnyValidated xs = or xs+  applyPolicy AllValidated [] = False+  applyPolicy AllValidated xs = and xs+  validate = (== Right True) . verifySig k p  verifySig :: JWK -> Types.Base64Octets -> Signature -> Either Error Bool-verifySig k m (Signature h (Types.Base64Octets s))-  = verify (headerAlg h) k (signingInput h m) s+verifySig k m sig@(Signature h _ (Types.Base64Octets s)) = maybe+  (Left $ AlgorithmMismatch "No 'alg' header")  -- shouldn't happen+  (\alg -> verify alg k (signingInput h m) s)+  (algorithm sig)
src/Crypto/JOSE/Legacy.hs view
@@ -28,7 +28,7 @@   ) where  import Control.Applicative-import Control.Arrow+import Data.Bifunctor  import Data.Aeson import Data.Aeson.Types
src/Crypto/JOSE/Types.hs view
@@ -33,6 +33,7 @@ import qualified Network.URI  import Crypto.JOSE.Types.Internal+import Crypto.JOSE.Types.Orphans ()   -- | A base64url encoded octet sequence interpreted as an integer.
+ src/Crypto/JOSE/Types/Armour.hs view
@@ -0,0 +1,85 @@+-- Copyright (C) 2014  Fraser Tweedale+--+-- Licensed under the Apache License, Version 2.0 (the "License");+-- you may not use this file except in compliance with the License.+-- You may obtain a copy of the License at+--+--      http://www.apache.org/licenses/LICENSE-2.0+--+-- Unless required by applicable law or agreed to in writing, software+-- distributed under the License is distributed on an "AS IS" BASIS,+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+-- See the License for the specific language governing permissions and+-- limitations under the License.++{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE UndecidableInstances #-}++{-|++Implementation of "armoured values" with partial decoding.++For cases where a value is parsed from some representation, but the+precise representation that was used is also needed.  The original+representation of a parsed value can be accessed using the 'armour'+function, but it cannot be changed.++-}+module Crypto.JOSE.Types.Armour+  (+    Armour(Unarmoured)+  , FromArmour(..)+  , ToArmour(..)+  , decodeArmour+  , armour+  , value+  ) where++import Control.Applicative+import Control.Monad ((>=>))++import Control.Lens+import Data.Aeson+++-- | A value that can be "armoured", where the armour representation+-- is preserved when the value is parsed.+--+data Armour a b+  = Armoured a b+  | Unarmoured b+  deriving (Show)++instance Eq b => Eq (Armour a b) where+  a == b = a ^. value == b ^. value++-- | Lens for the unarmoured value.+--+value :: Lens' (Armour a b) b+value = lens (\case Armoured _ b -> b ; Unarmoured b -> b) (const Unarmoured)++-- | 'Getter' for the armour encoding.  If the armour was+-- remembered, it is returned unchanged.+--+armour :: ToArmour a b => Getter (Armour a b) a+armour = to (\case Armoured a _ -> a ; Unarmoured b -> toArmour b)+++class FromArmour a e b | a b -> e where+  parseArmour :: a -> Either e b+++class ToArmour a b where+  toArmour :: b -> a+++-- | Decode an armoured value, remembering the armour.+--+decodeArmour :: FromArmour a e b => a -> Either e (Armour a b)+decodeArmour a = Armoured a <$> parseArmour a+++instance (FromJSON a, Show e, FromArmour a e b) => FromJSON (Armour a b) where+  parseJSON = parseJSON >=> either (fail . show) pure . decodeArmour
+ src/Crypto/JOSE/Types/Orphans.hs view
@@ -0,0 +1,32 @@+-- Copyright (C) 2014  Fraser Tweedale+--+-- Licensed under the Apache License, Version 2.0 (the "License");+-- you may not use this file except in compliance with the License.+-- You may obtain a copy of the License at+--+--      http://www.apache.org/licenses/LICENSE-2.0+--+-- Unless required by applicable law or agreed to in writing, software+-- distributed under the License is distributed on an "AS IS" BASIS,+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+-- See the License for the specific language governing permissions and+-- limitations under the License.++{-# OPTIONS_GHC -fno-warn-orphans #-}++module Crypto.JOSE.Types.Orphans where++import qualified Data.Traversable as T+import Data.List.NonEmpty (NonEmpty(..), toList)++import Data.Aeson++import qualified Data.Vector as V++instance FromJSON a => FromJSON (NonEmpty a) where+  parseJSON = withArray "NonEmpty [a]" $ \v -> case V.toList v of+    [] -> fail "Non-empty list required"+    (x:xs) -> T.mapM parseJSON (x :| xs)++instance ToJSON a => ToJSON (NonEmpty a) where+  toJSON = Array . V.fromList . map toJSON . toList
src/Crypto/JWT.hs view
@@ -31,12 +31,12 @@   , Audience(..)    , StringOrURI(..)-  , IntDate(..)+  , NumericDate(..)   ) where  import Control.Applicative-import Control.Arrow import Control.Monad+import Data.Bifunctor import Data.Maybe  import Data.Aeson@@ -72,14 +72,14 @@ -- | A JSON numeric value representing the number of seconds from --   1970-01-01T0:0:0Z UTC until the specified UTC date\/time. ---newtype IntDate = IntDate UTCTime deriving (Eq, Show)+newtype NumericDate = NumericDate UTCTime deriving (Eq, Show) -instance FromJSON IntDate where-  parseJSON = withScientific "IntDate" $-    pure . IntDate . posixSecondsToUTCTime . fromRational . toRational+instance FromJSON NumericDate where+  parseJSON = withScientific "NumericDate" $+    pure . NumericDate . posixSecondsToUTCTime . fromRational . toRational -instance ToJSON IntDate where-  toJSON (IntDate t)+instance ToJSON NumericDate where+  toJSON (NumericDate t)     = Number $ fromRational $ toRational $ utcTimeToPOSIXSeconds t  @@ -121,21 +121,21 @@   -- principal processing the claim does not identify itself with a   -- value in the /aud/ claim when this claim is present, then the   -- JWT MUST be rejected.-  , claimExp :: Maybe IntDate+  , claimExp :: Maybe NumericDate   -- ^ The expiration time claim identifies the expiration time on   -- or after which the JWT MUST NOT be accepted for processing.   -- The processing of /exp/ claim requires that the current   -- date\/time MUST be before expiration date\/time listed in the   -- /exp/ claim.  Implementers MAY provide for some small leeway,   -- usually no more than a few minutes, to account for clock skew.-  , claimNbf :: Maybe IntDate+  , claimNbf :: Maybe NumericDate   -- ^ The not before claim identifies the time before which the JWT   -- MUST NOT be accepted for processing.  The processing of the   -- /nbf/ claim requires that the current date\/time MUST be after   -- or equal to the not-before date\/time listed in the /nbf/   -- claim.  Implementers MAY provide for some small leeway, usually   -- no more than a few minutes, to account for clock skew.-  , claimIat :: Maybe IntDate+  , claimIat :: Maybe NumericDate   -- ^ The issued at claim identifies the time at which the JWT was   -- issued.  This claim can be used to determine the age of the   -- JWT.@@ -204,7 +204,7 @@ instance FromCompact JWT where   fromCompact = fromCompact >=> toJWT where     toJWT (JWTJWS jws) =-      either (Left . CompactDecodeError) (Right . JWT (JWTJWS jws))+      bimap CompactDecodeError (JWT (JWTJWS jws))         $ eitherDecode $ jwsPayload jws  instance ToCompact JWT where@@ -213,8 +213,13 @@  -- | Validate a JWT as a JWS (JSON Web Signature). ---validateJWSJWT :: JWK -> JWT -> Bool-validateJWSJWT k (JWT (JWTJWS jws) _) = verifyJWS k jws+validateJWSJWT+  :: ValidationAlgorithms+  -> ValidationPolicy+  -> JWK+  -> JWT+  -> Bool+validateJWSJWT algs policy k (JWT (JWTJWS jws) _) = verifyJWS algs policy k jws  -- | Create a JWT that is a JWS. --