-- This file is part of persona - Persona (BrowserID) library
-- Copyright (C) 2013, 2014, 2015, 2016 Fraser Tweedale
--
-- persona is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see .
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-|
Mozilla Persona (formerly BrowserID) types.
-}
module Crypto.Persona
(
RelativeURI()
, parseRelativeURI
, DelegatedSupportDocument(DelegatedSupportDocument)
, authority
, SupportDocument
, publicKey
, authentication
, provisioning
, supportDocument
, Principal(..)
, certify
, provisioningApiJsUrl
, authenticationApiJsUrl
) where
import Prelude hiding (exp)
import Control.Applicative
import Control.Lens hiding (iat, (.=))
import Data.Aeson
import Data.Default.Class (def)
import qualified Data.Text as T
import Data.Time
import Data.Time.Clock.POSIX
import Network.URI (URI, parseRelativeReference)
import Crypto.JOSE
import Crypto.JOSE.Legacy
import Crypto.JWT
-- | Newtype of URI resticted to relative URIs.
--
newtype RelativeURI = RelativeURI URI deriving (Eq)
instance Show RelativeURI where
show (RelativeURI uri) = show uri
instance FromJSON RelativeURI where
parseJSON = withText "URI" $
maybe (fail "not a relative URI") pure . parseRelativeURI . T.unpack
instance ToJSON RelativeURI where
toJSON (RelativeURI uri) = String $ T.pack $ show uri
-- | Construct a 'RelativeURI'
--
parseRelativeURI :: String -> Maybe RelativeURI
parseRelativeURI = fmap RelativeURI . Network.URI.parseRelativeReference
-- | Basic /support document/.
--
-- See .
--
data SupportDocument = SupportDocument
{ _publicKey :: JWK'
, _authentication :: RelativeURI
, _provisioning :: RelativeURI
}
makeLenses ''SupportDocument
instance FromJSON SupportDocument where
parseJSON = withObject "SupportDocument" (\o -> SupportDocument
<$> o .: "public-key"
<*> o .: "authentication"
<*> o .: "provisioning")
instance ToJSON SupportDocument where
toJSON (SupportDocument k a p) = object
[ "public-key" .= k
, "authentication" .= a
, "provisioning" .= p
]
-- | Construct a 'SupportDocument'
--
-- The smart constructor is needed to ensure that any private key
-- material is stripped from the key. Although RSA keys always have
-- public material the result is a 'Maybe SupportDocument' to enable
-- a move to the JSON Web Key (JWK) format.
--
supportDocument :: JWK' -> RelativeURI -> RelativeURI -> Maybe SupportDocument
supportDocument k a p =
SupportDocument k a p & publicKey (preview asPublicKey)
-- | /Delegated support document/
--
newtype DelegatedSupportDocument = DelegatedSupportDocument
{ _authority :: String
} deriving (Eq, Show)
makeLenses ''DelegatedSupportDocument
instance FromJSON DelegatedSupportDocument where
parseJSON = withObject "DelegatedSupportDocument" $ \o ->
DelegatedSupportDocument <$> o .: "authority"
instance ToJSON DelegatedSupportDocument where
toJSON (DelegatedSupportDocument s) = object [ "authority" .= s ]
-- | Persona identity principal
--
-- TODO: actually restrict to email addresses or hostnames.
--
data Principal = EmailPrincipal T.Text | HostPrincipal T.Text
instance FromJSON Principal where
parseJSON = withObject "Principal" (\o ->
EmailPrincipal <$> o .: "email"
<|> HostPrincipal <$> o .: "host")
instance ToJSON Principal where
toJSON (EmailPrincipal s) = object ["email" .= s]
toJSON (HostPrincipal s) = object ["host" .= s]
-- | Create an identity assertion.
--
certify
:: MonadRandom m
=> JWK' -- ^ Signing key
-> StringOrURI -- ^ Issuer
-> UTCTime
-- ^ Current time. Will be used for the "iat" claim and in the
-- calculation of the "exp" claim.
-> Integer
-- ^ Requested duration. Will be used in the calculation of the
-- "exp" claim.
-> Value -- ^ User public key object
-> Principal -- ^ Principal
-> m (Either Error JWT)
certify k iss t dur pk principal =
createJWSJWT (toJWK k) header claims
where
claims = emptyClaimsSet
& claimIss .~ Just iss
& claimExp .~ Just exp
& claimIat .~ Just iat
& addClaim "public-key" (toJSON pk)
& addClaim "principal" (toJSON principal)
header = def { headerAlg = Just RS256 }
-- SHOULD NOT issue cert valid longer than duration
-- MUST NOT issue cert valid longer than 24 hours
exp = toMs $ addUTCTime (fromRational $ toRational $ min dur 86400) t
iat = toMs t
toMs = NumericDate . posixSecondsToUTCTime . (* 1000) . utcTimeToPOSIXSeconds
-- | URI to official provisioning JavaScript.
--
provisioningApiJsUrl :: String
provisioningApiJsUrl = "https://login.persona.org/provisioning_api.js"
-- | URI to official authentication JavaScript.
--
authenticationApiJsUrl :: String
authenticationApiJsUrl = "https://login.persona.org/provisioning_api.js"