Skip to content

Commit

Permalink
improve header protection ergonomics (breaking change)
Browse files Browse the repository at this point in the history
Changed the header protection data types for better ergonomics
([#125](#125)).
Previously, `()` was used for serialisations that only support
protected headers (thus, a single constructor).  This release
introduces the new single-constructor data type `ProtectedOnly` to
replace the use of `()` for this purpose.  This is a breaking
change and some library users will need to update their code.

The `Protection` type has been renamed to `ProtectionOptional`,
with the old name retained as a (deprecated) type synonym.

The `ProtectionIndicator` class has been renamed to
`ProtectionOptionality`, with the old name retained as a
(deprecated) type synonym.

Added some convenience header and header parameter constructors:
`newJWSHeaderProtected`, `newHeaderParamProtected` and
`newHeaderParamUnprotected`.

Fixes: #125
  • Loading branch information
frasertweedale committed Mar 29, 2024
1 parent c2f6690 commit 230f432
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 62 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
## Version NEXT

- Changed the header protection data types for better ergonomics
([#125](https://github.com/frasertweedale/hs-jose/issues/125)).
Previously, `()` was used for serialisations that only support
protected headers (thus, a single constructor). This release
introduces the new single-constructor data type `ProtectedOnly` to
replace the use of `()` for this purpose. This is a breaking
change and some library users will need to update their code.

The `Protection` type has been renamed to `ProtectionOptional`,
with the old name retained as a (deprecated) type synonym.

The `ProtectionIndicator` class has been renamed to
`ProtectionOptionality`, with the old name retained as a
(deprecated) type synonym.

Added some convenience header and header parameter constructors:
`newJWSHeaderProtected`, `newHeaderParamProtected` and
`newHeaderParamUnprotected`.

- Generalised the types of `signJWT`, `verifyJWT` and related
functions to accept custom JWS header types. Added new type
synonym `SignedJWTWithHeader h`. This change could break some
Expand Down
81 changes: 60 additions & 21 deletions src/Crypto/JOSE/Header.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}

Expand All @@ -22,10 +23,19 @@ Types and functions for working with JOSE header parameters.
-}
module Crypto.JOSE.Header
(
-- * Defining header data types
-- * Constructing header parameters
HeaderParam(..)
, ProtectionIndicator(..)
, Protection(..)
, newHeaderParamProtected
, newHeaderParamUnprotected

-- ** Optionality of header protection
, ProtectionOptionality(..)
, ProtectionIndicator

, ProtectionOptional(..)
, ProtectedOnly(..)
, Protection

, protection
, isProtected
, param
Expand Down Expand Up @@ -87,15 +97,15 @@ import qualified Crypto.JOSE.Types as Types
class HasParams (a :: Type -> Type) where
-- | Return a list of parameters,
-- each paired with whether it is protected or not.
params :: ProtectionIndicator p => a p -> [(Bool, Pair)]
params :: ProtectionOptionality p => a p -> [(Bool, Pair)]

-- | List of "known extensions", i.e. keys that may appear in the
-- "crit" header parameter.
extensions :: Proxy a -> [T.Text]
extensions = const []

parseParamsFor
:: (HasParams b, ProtectionIndicator p)
:: (HasParams b, ProtectionOptionality p)
=> Proxy b -> Maybe Object -> Maybe Object -> Parser (a p)

-- | Parse a pair of objects (protected and unprotected header)
Expand All @@ -105,14 +115,14 @@ class HasParams (a :: Type -> Type) where
-- to access "known extensions" understood by the target type.)
--
parseParams
:: forall a p. (HasParams a, ProtectionIndicator p)
:: forall a p. (HasParams a, ProtectionOptionality p)
=> Maybe Object -- ^ protected header
-> Maybe Object -- ^ unprotected header
-> Parser (a p)
parseParams = parseParamsFor (Proxy :: Proxy a)

protectedParams
:: (HasParams a, ProtectionIndicator p)
:: (HasParams a, ProtectionOptionality p)
=> a p -> Maybe Value {- ^ Object -}
protectedParams h =
case (map snd . filter fst . params) h of
Expand All @@ -122,40 +132,60 @@ protectedParams h =
-- | Return the base64url-encoded protected parameters
--
protectedParamsEncoded
:: (HasParams a, ProtectionIndicator p)
:: (HasParams a, ProtectionOptionality p)
=> a p -> L.ByteString
protectedParamsEncoded =
maybe mempty (review base64url . encode) . protectedParams

-- | Return unprotected params as a JSON 'Value' (always an object)
--
unprotectedParams
:: (HasParams a, ProtectionIndicator p)
:: (HasParams a, ProtectionOptionality p)
=> a p -> Maybe Value {- ^ Object -}
unprotectedParams h =
case (map snd . filter (not . fst) . params) h of
[] -> Nothing
xs -> Just (object xs)

-- | Whether a header is protected or unprotected
--
data Protection = Protected | Unprotected
deriving (Eq, Show)

class Eq a => ProtectionIndicator a where
-- | Class that defines the protected and (if supported) unprotected values
-- for a protection indicator data type.
--
class Eq a => ProtectionOptionality a where
-- | Get a value for indicating protection.
getProtected :: a

-- | Get a 'Just' a value for indicating no protection, or 'Nothing'
-- if the type does not support unprotected headers.
getUnprotected :: Maybe a

instance ProtectionIndicator Protection where
type ProtectionIndicator = ProtectionOptionality
{-# DEPRECATED ProtectionIndicator "renamed to 'ProtectionOptionality'." #-}



-- | Use this protection type when the serialisation supports both
-- protected and unprotected headers.
--
data ProtectionOptional = Protected | Unprotected
deriving (Eq, Show)

instance ProtectionOptionality ProtectionOptional where
getProtected = Protected
getUnprotected = Just Unprotected

instance ProtectionIndicator () where
getProtected = ()
type Protection = ProtectionOptional
{-# DEPRECATED Protection "renamed to 'ProtectionOptional'." #-}


-- | Use this protection type when the serialisation only supports
-- protected headers.
--
data ProtectedOnly = ProtectedOnly
deriving (Eq, Show)

instance ProtectionOptionality ProtectedOnly where
getProtected = ProtectedOnly
getUnprotected = Nothing


Expand All @@ -178,10 +208,19 @@ param f (HeaderParam p v) = fmap (\v' -> HeaderParam p v') (f v)
{-# ANN param "HLint: ignore Avoid lambda" #-}

-- | Getter for whether a parameter is protected
isProtected :: (ProtectionIndicator p) => Getter (HeaderParam p a) Bool
isProtected :: (ProtectionOptionality p) => Getter (HeaderParam p a) Bool
isProtected = protection . to (== getProtected)


-- | Convenience constructor for a protected 'HeaderParam'.
newHeaderParamProtected :: (ProtectionIndicator p) => a -> HeaderParam p a
newHeaderParamProtected = HeaderParam getProtected

-- | Convenience constructor for a protected 'HeaderParam'.
newHeaderParamUnprotected :: a -> HeaderParam ProtectionOptional a
newHeaderParamUnprotected = HeaderParam Unprotected


{- $parsing
The 'parseParamsFor' function defines the parser for a header type.
Expand Down Expand Up @@ -224,7 +263,7 @@ instance HasParams ACMEHeader where
-- the protected or the unprotected header.
--
headerOptional
:: (FromJSON a, ProtectionIndicator p)
:: (FromJSON a, ProtectionOptionality p)
=> T.Text
-> Maybe Object
-> Maybe Object
Expand All @@ -236,7 +275,7 @@ headerOptional = headerOptional' parseJSON
-- but with an explicit argument for the parser.
--
headerOptional'
:: (ProtectionIndicator p)
:: (ProtectionOptionality p)
=> (Value -> Parser a)
-> T.Text
-> Maybe Object
Expand Down Expand Up @@ -274,7 +313,7 @@ headerOptionalProtected kText hp hu = case (hp >>= M.lookup k, hu >>= M.lookup k
-- the protected or the unprotected header.
--
headerRequired
:: (FromJSON a, ProtectionIndicator p)
:: (FromJSON a, ProtectionOptionality p)
=> T.Text
-> Maybe Object
-> Maybe Object
Expand Down
6 changes: 3 additions & 3 deletions src/Crypto/JOSE/JWE.hs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ data JWEHeader p = JWEHeader
}
deriving (Eq, Show)

newJWEHeader :: ProtectionIndicator p => AlgWithParams -> Enc -> JWEHeader p
newJWEHeader :: (ProtectionOptionality p) => AlgWithParams -> Enc -> JWEHeader p
newJWEHeader alg enc =
JWEHeader (Just alg) (HeaderParam getProtected enc) z z z z z z z z z z z
where z = Nothing
Expand Down Expand Up @@ -134,7 +134,7 @@ instance FromJSON (JWERecipient a p) where
<*> o .:? "encrypted_key"

parseRecipient
:: (HasParams a, ProtectionIndicator p)
:: (HasParams a, ProtectionOptionality p)
=> Maybe Object -> Maybe Object -> Value -> Parser (JWERecipient a p)
parseRecipient hp hu = withObject "JWE Recipient" $ \o -> do
hr <- o .:? "header"
Expand All @@ -153,7 +153,7 @@ data JWE a p = JWE
, _jweRecipients :: [JWERecipient a p]
}

instance (HasParams a, ProtectionIndicator p) => FromJSON (JWE a p) where
instance (HasParams a, ProtectionOptionality p) => FromJSON (JWE a p) where
parseJSON = withObject "JWE JSON Serialization" $ \o -> do
hpB64 <- o .:? "protected"
hp <- maybe
Expand Down
Loading

0 comments on commit 230f432

Please sign in to comment.