jose 0.8.3.1 → 0.8.4
raw patch · 6 files changed
+89/−87 lines, 6 filesdep ~base64-bytestringPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: base64-bytestring
API changes (from Hackage documentation)
+ Crypto.JOSE.JWS: rawProtectedHeader :: (HasParams a, ProtectionIndicator p) => Signature p a -> ByteString
- Crypto.JWT: _JWSError :: AsJWTError r_a1fD6 => Prism' r_a1fD6 Error
+ Crypto.JWT: _JWSError :: AsJWTError r_a1fhJ => Prism' r_a1fhJ Error
- Crypto.JWT: _JWTClaimsSetDecodeError :: AsJWTError r_a1fD6 => Prism' r_a1fD6 String
+ Crypto.JWT: _JWTClaimsSetDecodeError :: AsJWTError r_a1fhJ => Prism' r_a1fhJ String
- Crypto.JWT: _JWTError :: AsJWTError r_a1fD6 => Prism' r_a1fD6 JWTError
+ Crypto.JWT: _JWTError :: AsJWTError r_a1fhJ => Prism' r_a1fhJ JWTError
- Crypto.JWT: _JWTExpired :: AsJWTError r_a1fD6 => Prism' r_a1fD6 ()
+ Crypto.JWT: _JWTExpired :: AsJWTError r_a1fhJ => Prism' r_a1fhJ ()
- Crypto.JWT: _JWTIssuedAtFuture :: AsJWTError r_a1fD6 => Prism' r_a1fD6 ()
+ Crypto.JWT: _JWTIssuedAtFuture :: AsJWTError r_a1fhJ => Prism' r_a1fhJ ()
- Crypto.JWT: _JWTNotInAudience :: AsJWTError r_a1fD6 => Prism' r_a1fD6 ()
+ Crypto.JWT: _JWTNotInAudience :: AsJWTError r_a1fhJ => Prism' r_a1fhJ ()
- Crypto.JWT: _JWTNotInIssuer :: AsJWTError r_a1fD6 => Prism' r_a1fD6 ()
+ Crypto.JWT: _JWTNotInIssuer :: AsJWTError r_a1fhJ => Prism' r_a1fhJ ()
- Crypto.JWT: _JWTNotYetValid :: AsJWTError r_a1fD6 => Prism' r_a1fD6 ()
+ Crypto.JWT: _JWTNotYetValid :: AsJWTError r_a1fhJ => Prism' r_a1fhJ ()
- Crypto.JWT: class AsJWTError r_a1fD6
+ Crypto.JWT: class AsJWTError r_a1fhJ
- Crypto.JWT: class HasJWTValidationSettings c_a1gnU
+ Crypto.JWT: class HasJWTValidationSettings c_a1g2x
- Crypto.JWT: jWTValidationSettings :: HasJWTValidationSettings c_a1gnU => Lens' c_a1gnU JWTValidationSettings
+ Crypto.JWT: jWTValidationSettings :: HasJWTValidationSettings c_a1g2x => Lens' c_a1g2x JWTValidationSettings
- Crypto.JWT: jwtValidationSettingsAllowedSkew :: HasJWTValidationSettings c_a1gnU => Lens' c_a1gnU NominalDiffTime
+ Crypto.JWT: jwtValidationSettingsAllowedSkew :: HasJWTValidationSettings c_a1g2x => Lens' c_a1g2x NominalDiffTime
- Crypto.JWT: jwtValidationSettingsAudiencePredicate :: HasJWTValidationSettings c_a1gnU => Lens' c_a1gnU (StringOrURI -> Bool)
+ Crypto.JWT: jwtValidationSettingsAudiencePredicate :: HasJWTValidationSettings c_a1g2x => Lens' c_a1g2x (StringOrURI -> Bool)
- Crypto.JWT: jwtValidationSettingsCheckIssuedAt :: HasJWTValidationSettings c_a1gnU => Lens' c_a1gnU Bool
+ Crypto.JWT: jwtValidationSettingsCheckIssuedAt :: HasJWTValidationSettings c_a1g2x => Lens' c_a1g2x Bool
- Crypto.JWT: jwtValidationSettingsIssuerPredicate :: HasJWTValidationSettings c_a1gnU => Lens' c_a1gnU (StringOrURI -> Bool)
+ Crypto.JWT: jwtValidationSettingsIssuerPredicate :: HasJWTValidationSettings c_a1g2x => Lens' c_a1g2x (StringOrURI -> Bool)
- Crypto.JWT: jwtValidationSettingsValidationSettings :: HasJWTValidationSettings c_a1gnU => Lens' c_a1gnU ValidationSettings
+ Crypto.JWT: jwtValidationSettingsValidationSettings :: HasJWTValidationSettings c_a1g2x => Lens' c_a1g2x ValidationSettings
Files
- jose.cabal +2/−2
- src/Crypto/JOSE/Header.hs +4/−5
- src/Crypto/JOSE/JWS.hs +32/−15
- src/Crypto/JOSE/Types/Internal.hs +2/−63
- test/JWS.hs +47/−0
- test/Types.hs +2/−2
jose.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: jose-version: 0.8.3.1+version: 0.8.4 synopsis: Javascript Object Signing and Encryption and JSON Web Token library description:@@ -79,7 +79,7 @@ build-depends: attoparsec- , base64-bytestring >= 1.0 && < 1.3+ , base64-bytestring >= 1.1 && < 1.3 , concise >= 0.1 , containers >= 0.5 , cryptonite >= 0.7
src/Crypto/JOSE/Header.hs view
@@ -67,10 +67,9 @@ import Data.Monoid ((<>)) import Data.Proxy (Proxy(..)) -import Control.Lens (Lens', Getter, to)+import Control.Lens (Lens', Getter, review, to) import Data.Aeson (FromJSON(..), Object, Value, encode, object) import Data.Aeson.Types (Pair, Parser)-import qualified Data.ByteString.Base64.URL.Lazy as B64UL import qualified Data.ByteString.Lazy as L import qualified Data.HashMap.Strict as M import qualified Data.Text as T@@ -78,7 +77,7 @@ import qualified Crypto.JOSE.JWA.JWS as JWA.JWS import Crypto.JOSE.JWK (JWK) import Crypto.JOSE.Types.Orphans ()-import Crypto.JOSE.Types.Internal (unpad)+import Crypto.JOSE.Types.Internal (base64url) import qualified Crypto.JOSE.Types as Types @@ -119,13 +118,13 @@ [] -> Nothing xs -> Just (object xs) --- | Return the encoded protected parameters+-- | Return the base64url-encoded protected parameters -- protectedParamsEncoded :: (HasParams a, ProtectionIndicator p) => a p -> L.ByteString protectedParamsEncoded =- maybe mempty (unpad . B64UL.encode . encode) . protectedParams+ maybe mempty (review base64url . encode) . protectedParams -- | Return unprotected params as a JSON 'Value' (always an object) --
src/Crypto/JOSE/JWS.hs view
@@ -1,4 +1,4 @@--- Copyright (C) 2013, 2014, 2015, 2016 Fraser Tweedale+-- Copyright (C) 2013, 2014, 2015, 2016, 2020 Fraser Tweedale -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License.@@ -73,6 +73,7 @@ , Signature , header , signature+ , rawProtectedHeader -- * JWS headers , Alg(..)@@ -313,9 +314,9 @@ ) instance (HasParams a, ProtectionIndicator p) => ToJSON (Signature p a) where- toJSON (Signature _ h sig) =+ toJSON s@(Signature _ h sig) = let- pro = case protectedParamsEncoded h of+ pro = case rawProtectedHeader s of "" -> id bs -> ("protected" .= String (T.decodeUtf8 (view recons bs)) :) unp = case unprotectedParams h of@@ -430,22 +431,32 @@ signingInput :: (HasParams a, ProtectionIndicator p)- => Either T.Text (a p)- -> B.ByteString+ => Signature p a+ -> Types.Base64Octets -> B.ByteString-signingInput h p = B.intercalate "."- [ either T.encodeUtf8 (view recons . protectedParamsEncoded) h- , review Types.base64url p- ]+signingInput sig (Types.Base64Octets p) =+ rawProtectedHeader sig <> "." <> review Types.base64url p +-- | Return the raw base64url-encoded protected header value.+-- If the Signature was decoded from JSON, this returns the+-- original string value as-is.+--+-- Application code should never need to use this. It is exposed+-- for testing purposes.+rawProtectedHeader+ :: (HasParams a, ProtectionIndicator p)+ => Signature p a -> B.ByteString+rawProtectedHeader (Signature raw h _) =+ maybe (view recons $ protectedParamsEncoded h) T.encodeUtf8 raw+ -- Convert JWS to compact serialization. -- -- The operation is defined only when there is exactly one -- signature and returns Nothing otherwise -- instance HasParams a => ToCompact (JWS Identity () a) where- toCompact (JWS (Types.Base64Octets p) (Identity (Signature raw h (Types.Base64Octets sig)))) =- [ view recons $ signingInput (maybe (Right h) Left raw) p+ toCompact (JWS p (Identity s@(Signature _ _ (Types.Base64Octets sig)))) =+ [ view recons $ signingInput s p , review Types.base64url sig ] @@ -487,8 +498,14 @@ ) => B.ByteString -> a p -> JWK -> m (Signature p a) mkSignature p h k =- Signature Nothing h . Types.Base64Octets- <$> sign (view (alg . param) h) (k ^. jwkMaterial) (signingInput (Right h) p)+ let+ almostSig = Signature Nothing h . Types.Base64Octets+ in+ almostSig+ <$> sign+ (view (alg . param) h)+ (k ^. jwkMaterial)+ (signingInput (almostSig "") (Types.Base64Octets p)) -- | Validation policy.@@ -633,7 +650,7 @@ -> Signature p a -> JWK -> Either Error Bool-verifySig (Types.Base64Octets m) (Signature raw h (Types.Base64Octets s)) k =+verifySig msg sig@(Signature _ h (Types.Base64Octets s)) k = verify (view (alg . param) h) (view jwkMaterial k) tbs s where- tbs = signingInput (maybe (Right h) Left raw) m+ tbs = signingInput sig msg
src/Crypto/JOSE/Types/Internal.hs view
@@ -28,8 +28,6 @@ , parseB64 , encodeB64Url , parseB64Url- , pad- , unpad , bsToInteger , integerToBS , intBytes@@ -38,7 +36,6 @@ ) where import Data.Bifunctor (first)-import Data.Char (ord) import Data.Monoid ((<>)) import Data.Tuple (swap) import Data.Word (Word8)@@ -48,7 +45,6 @@ import Crypto.Number.Basic (log2) import Data.Aeson.Types import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64.URL as B64U import qualified Data.HashMap.Strict as M@@ -74,63 +70,7 @@ encodeB64 :: B.ByteString -> Value encodeB64 = String . E.decodeUtf8 . B64.encode -class IsChar a where- fromChar :: Char -> a -instance IsChar Char where- fromChar = id--instance IsChar Word8 where- fromChar = fromIntegral . ord---- | Add appropriate base64 '=' padding.----pad :: (Snoc s s a a, IsChar a) => s -> s-pad = rpad 4 (fromChar '=')-{-# INLINE [2] pad #-}--rpad :: (Snoc s s a a) => Int -> a -> s -> s-rpad w a s =- let n = ((w - snocLength s `mod` w) `mod` w)- in foldr (.) id (replicate n (`snoc` a)) s-{-# INLINE rpad #-}--snocLength :: (Snoc s s a a) => s -> Int-snocLength s = case unsnoc s of- Nothing -> 0- Just (s', _) -> 1 + snocLength s'-{-# INLINE snocLength #-}--padB :: B.ByteString -> B.ByteString-padB s = s <> B.replicate ((4 - B.length s `mod` 4) `mod` 4) 61-{-# RULES "pad/padB" pad = padB #-}--padL :: L.ByteString -> L.ByteString-padL s = s <> L.replicate ((4 - L.length s `mod` 4) `mod` 4) 61-{-# RULES "pad/padL" pad = padL #-}----- | Strip base64 '=' padding.----unpad :: (Snoc s s a a, IsChar a, Eq a) => s -> s-unpad = rstrip (== fromChar '=')-{-# INLINE [2] unpad #-}--rstrip :: (Snoc s s a a) => (a -> Bool) -> s -> s-rstrip p s = case unsnoc s of- Nothing -> s- Just (s', a) -> if p a then rstrip p s' else s-{-# INLINE rstrip #-}--unpadB :: B.ByteString -> B.ByteString-unpadB = B.reverse . B.dropWhile (== 61) . B.reverse-{-# RULES "unpad/unpadB" unpad = unpadB #-}--unpadL :: L.ByteString -> L.ByteString-unpadL = L.reverse . L.dropWhile (== 61) . L.reverse-{-# RULES "unpad/unpadL" unpad = unpadL #-}-- -- | Prism for encoding / decoding base64url. -- -- To encode, @'review' base64url@.@@ -143,10 +83,9 @@ , Cons s1 s1 Word8 Word8 , Cons s2 s2 Word8 Word8 ) => Prism' s1 s2-base64url = reconsIso . padder . b64u . reconsIso+base64url = reconsIso . b64u . reconsIso where- padder = iso pad unpad- b64u = prism B64U.encode (\s -> first (const s) (B64U.decode s))+ b64u = prism B64U.encodeUnpadded (\s -> first (const s) (B64U.decodeUnpadded s)) reconsIso = iso (view recons) (view recons)
test/JWS.hs view
@@ -50,6 +50,8 @@ appendixA5Spec appendixA6Spec cfrgSpec+ base64urlSpec+ reserialiseSpec -- Extension of JWSHeader to test "crit" behaviour@@ -519,3 +521,48 @@ it "validates signatures correctly" $ verify JWA.JWS.EdDSA (view jwkMaterial jwk) signingInput sig `shouldBe` (Right True :: Either Error Bool)++base64urlSpec :: Spec+base64urlSpec = describe "base64url-decoding behaviour" $+ it "rejects baseurl-encoded values with padding" $+ -- from https://github.com/frasertweedale/hs-jose/issues/99+ -- and https://github.com/frasertweedale/hs-jose/issues/97+ let+ inJws = "{\"signature\":\"fake\",\"protected\":\"eyJraWQiOiI1ODJhMTdkZS0zYjg4LTQxZTgtOGM3MC0zZjBkNmYwMDNmY2QiLCJhbGciOiJSUzI1NiJ9\",\"payload\":\"eyJ1c2VybmFtZSI6Imp1c3BheSIsImdzdGluIjoiMjBhYWFhYTEyMzRhMWEyIn0=\"}"+ in+ (eitherDecode inJws :: Either String (FlattenedJWS JWSHeader))+ `shouldSatisfy` is _Left++reserialiseSpec :: Spec+reserialiseSpec = describe "reserialisation of JWS" $+ it "preserves raw protected header value" $ do+ -- https://github.com/frasertweedale/hs-jose/issues/98+ --+ -- Test two different JWKs with same protected header fields+ -- but different order. For each input parse then reserialise,+ -- then parse again and ensure that the raw protected header+ -- string is the same for both parses.+ --+ -- The two strings with different orders are needed to test+ -- that we preserve and use the recorded raw protected header+ -- when reserialising the object. (We don't know what order+ -- the keys will be in the aeson object hashmap, so we test+ -- with two different orders to avoid a false success).+ let+ -- {"kid":"0","alg":"RS256"}+ s1 = "{\"protected\":\"eyJraWQiOiIwIiwiYWxnIjoiUlMyNTYifQ\",\"payload\":\"\",\"signature\":\"\"}"+ -- {"alg":"RS256","kid":"0"}+ s2 = "{\"signature\":\"\",\"protected\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IjAifQ\",\"payload\":\"\"}"++ check s = do+ let+ jws = eitherDecode s :: Either String (FlattenedJWS JWSHeader)+ s' = encode <$> jws+ jws' = s' >>= eitherDecode :: Either String (FlattenedJWS JWSHeader)+ jws `shouldSatisfy` is _Right+ toListOf (signatures . to rawProtectedHeader) <$> jws+ `shouldBe`+ toListOf (signatures . to rawProtectedHeader) <$> jws'++ check s1+ check s2
test/Types.hs view
@@ -35,8 +35,8 @@ base64OctetsSpec :: Spec base64OctetsSpec = describe "Base64Octets" $ it "can be read from JSON" $ do- decode "[\"AxY8DCtDaGlsbGljb3RoZQ\"]" `shouldBe` Just [Base64Octets iv]- decode "[\"9hH0vgRfYgPnAHOd8stkvw\"]" `shouldBe` Just [Base64Octets tag]+ eitherDecode "[\"AxY8DCtDaGlsbGljb3RoZQ\"]" `shouldBe` Right [Base64Octets iv]+ eitherDecode "[\"9hH0vgRfYgPnAHOd8stkvw\"]" `shouldBe` Right [Base64Octets tag] where iv = BS.pack [3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101] tag = BS.pack [246, 17, 244, 190, 4, 95, 98, 3, 231, 0, 115, 157, 242, 203, 100, 191]