packages feed

biscuit-haskell-0.1.0.0: src/Auth/Biscuit/Token.hs

{-# LANGUAGE NamedFieldPuns  #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections   #-}
{-|
  Module      : Auth.Biscuit.Token
  Copyright   : © Clément Delafargue, 2021
  License     : MIT
  Maintainer  : clement@delafargue.name
  Module defining the main biscuit-related operations
-}
module Auth.Biscuit.Token
  ( Biscuit (..)
  , ParseError (..)
  , VerificationError (..)
  , ExistingBlock
  , mkBiscuit
  , addBlock
  , checkBiscuitSignature
  , parseBiscuit
  , serializeBiscuit
  , verifyBiscuit
  , verifyBiscuitWithLimits

  , BlockWithRevocationIds (..)
  , getRevocationIds
  ) where

import           Control.Monad                 (when)
import           Control.Monad.Except          (runExceptT, throwError)
import           Control.Monad.IO.Class        (liftIO)
import           Data.Bifunctor                (first)
import           Data.ByteString               (ByteString)
import           Data.List.NonEmpty            (NonEmpty ((:|)))
import qualified Data.List.NonEmpty            as NE

import           Auth.Biscuit.Datalog.AST      (Block, Query, Verifier)
import           Auth.Biscuit.Datalog.Executor (BlockWithRevocationIds (..),
                                                ExecutionError, Limits,
                                                defaultLimits,
                                                runVerifierWithLimits)
import qualified Auth.Biscuit.Proto            as PB
import           Auth.Biscuit.ProtoBufAdapter  (Symbols, blockToPb,
                                                commonSymbols, extractSymbols,
                                                pbToBlock)
import           Auth.Biscuit.Sel              (Keypair (publicKey), PublicKey,
                                                Signature (..), aggregate,
                                                hashBytes, newKeypair,
                                                parsePublicKey,
                                                serializePublicKey, signBlock,
                                                verifySignature)
import           Auth.Biscuit.Utils            (maybeToRight)

-- | Protobuf serialization does not have a guaranteed deterministic behaviour,
-- so we need to keep the initial serialized payload around in order to compute
-- a new signature when adding a block.
type ExistingBlock = (ByteString, Block)

-- | A parsed biscuit
data Biscuit
  = Biscuit
  { symbols   :: Symbols
  -- ^ The symbols already defined in the contained blocks
  , authority :: (PublicKey, ExistingBlock)
  -- ^ The authority block, along with the associated public key. The public key
  -- is kept around since it's embedded in the serialized biscuit, but should not
  -- be used for verification. An externally provided public key should be used instead.
  , blocks    :: [(PublicKey, ExistingBlock)]
  -- ^ The extra blocks, along with the public keys needed
  , signature :: Signature
  }
  deriving (Eq, Show)

-- | Create a new biscuit with the provided authority block
mkBiscuit :: Keypair -> Block -> IO Biscuit
mkBiscuit keypair authority = do
  let authorityPub = publicKey keypair
      (s, authoritySerialized) = PB.encodeBlock <$> blockToPb commonSymbols 0 authority
  signature <- signBlock keypair authoritySerialized
  pure $ Biscuit { authority = (authorityPub, (authoritySerialized, authority))
                 , blocks = []
                 , symbols = commonSymbols <> s
                 , signature
                 }

-- | Add a block to an existing biscuit. The block will be signed
-- with a randomly-generated keypair
addBlock :: Block -> Biscuit -> IO Biscuit
addBlock newBlock b@Biscuit{..} = do
  let (s, newBlockSerialized) = PB.encodeBlock <$> blockToPb symbols (length blocks) newBlock
  keypair <- newKeypair
  newSig <- signBlock keypair newBlockSerialized
  endSig <- aggregate signature newSig
  pure $ b { blocks = blocks <> [(publicKey keypair, (newBlockSerialized, newBlock))]
           , symbols = symbols <> s
           , signature = endSig
           }

-- | Only check a biscuit signature. This can be used to perform an early check, before
-- bothering with constructing a verifier.
checkBiscuitSignature :: Biscuit -> PublicKey -> IO Bool
checkBiscuitSignature Biscuit{..} publicKey =
  let publicKeysAndMessages = (publicKey, fst $ snd authority) :| (fmap fst <$> blocks)
   in verifySignature publicKeysAndMessages signature

-- | Errors that can happen when parsing a biscuit
data ParseError
  = InvalidHexEncoding
  -- ^ The provided ByteString is not hex-encoded
  | InvalidB64Encoding
  -- ^ The provided ByteString is not base64-encoded
  | InvalidProtobufSer String
  -- ^ The provided ByteString does not contain properly serialized protobuf values
  | InvalidProtobuf String
  -- ^ The bytestring was correctly deserialized from protobuf, but the values can't be turned into a proper biscuit
  deriving (Eq, Show)

-- | Parse a biscuit from a raw bytestring.
parseBiscuit :: ByteString -> Either ParseError Biscuit
parseBiscuit bs = do
  blockList <- first InvalidProtobufSer $ PB.decodeBlockList bs
  let pbBlocks    = PB.getField $ PB.blocks    blockList
      pbKeys      = PB.getField $ PB.keys      blockList
      pbAuthority = PB.getField $ PB.authority blockList
      pbSignature = PB.getField $ PB.signature blockList
  when (length pbBlocks + 1 /= length pbKeys) $ Left (InvalidProtobufSer $ "Length mismatch " <> show (length pbBlocks, length pbKeys))
  rawAuthority <- first InvalidProtobufSer $ PB.decodeBlock pbAuthority
  rawBlocks    <- traverse (first InvalidProtobufSer . PB.decodeBlock) pbBlocks
  let s = extractSymbols commonSymbols $ rawAuthority : rawBlocks


  parsedAuthority <- (pbAuthority,) <$> blockFromPB s rawAuthority
  parsedBlocks    <- zip pbBlocks <$> traverse (blockFromPB s) rawBlocks
  parsedKeys      <- maybeToRight (InvalidProtobufSer "Invalid pubkeys") $ traverse parsePublicKey pbKeys
  let blocks = zip (drop 1 parsedKeys) parsedBlocks
      authority = (head parsedKeys, parsedAuthority)
      symbols = s
      signature = Signature { parameters = PB.getField $ PB.parameters pbSignature
                            , z = PB.getField $ PB.z pbSignature
                            }
  pure Biscuit{..}

-- | Serialize a biscuit to a raw bytestring
serializeBiscuit :: Biscuit -> ByteString
serializeBiscuit Biscuit{..} =
  let authorityBs = fst $ snd authority
      blocksBs = fst . snd <$> blocks
      keys = serializePublicKey . fst <$> authority : blocks
      Signature{..} = signature
      sigPb = PB.Signature
                { parameters = PB.putField parameters
                , z = PB.putField z
                }
   in PB.encodeBlockList PB.Biscuit
       { authority = PB.putField authorityBs
       , blocks    = PB.putField blocksBs
       , keys      = PB.putField keys
       , signature = PB.putField sigPb
       }

-- | Parse a single block from a protobuf value
blockFromPB :: Symbols -> PB.Block -> Either ParseError Block
blockFromPB s pbBlock  = first InvalidProtobuf $ pbToBlock s pbBlock

-- | An error that can happen when verifying a biscuit
data VerificationError
  = SignatureError
  -- ^ The signature is invalid
  | DatalogError ExecutionError
  -- ^ The checks and policies could not be verified
  deriving (Eq, Show)

-- | Given a provided verifier (a set of facts, rules, checks and policies),
-- and a public key, verify a biscuit:
--
-- - make sure the biscuit has been signed with the private key associated to the public key
-- - make sure the biscuit is valid for the provided verifier
verifyBiscuitWithLimits :: Limits -> Biscuit -> Verifier -> PublicKey -> IO (Either VerificationError Query)
verifyBiscuitWithLimits l b verifier pub = runExceptT $ do
  sigCheck <- liftIO $ checkBiscuitSignature b pub
  when (not sigCheck) $ throwError SignatureError
  authorityBlock :| attBlocks <- liftIO $ getRevocationIds b
  verifResult <- liftIO $ runVerifierWithLimits l authorityBlock attBlocks verifier
  case verifResult of
    Left e  -> throwError $ DatalogError e
    Right p -> pure p

-- | Same as `verifyBiscuitWithLimits`, but with default limits (1ms timeout, max 1000 facts, max 100 iterations)
verifyBiscuit :: Biscuit -> Verifier -> PublicKey -> IO (Either VerificationError Query)
verifyBiscuit = verifyBiscuitWithLimits defaultLimits

-- | Get the components needed to compute revocation ids
getRidComponents :: (PublicKey, ExistingBlock) -> ByteString
                 -> ((ByteString, ByteString), Block)
getRidComponents (pub, (blockBs, block)) param =
  ( ( blockBs <> serializePublicKey pub
    , blockBs <> serializePublicKey pub <> param
    )
  , block
  )

-- | Given revocation ids components and a block, compute the revocation ids
-- and attach them to the block
mkBRID :: ((ByteString, ByteString), Block) -> IO BlockWithRevocationIds
mkBRID ((g,u), bBlock) = do
  genericRevocationId <- hashBytes g
  uniqueRevocationId  <- hashBytes u
  pure BlockWithRevocationIds{..}

-- | Compute the revocation ids for a given biscuit
getRevocationIds :: Biscuit -> IO (NonEmpty BlockWithRevocationIds)
getRevocationIds Biscuit{..} = do
   params <- maybe (fail "") pure . NE.nonEmpty $ parameters signature
   let allBlocks = authority :| blocks
       blocksAndParams = NE.zipWith getRidComponents allBlocks params
       conc ((g1, u1), _) ((g2, u2), b) = ((g1 <> g2, u1 <> u2), b)
       withPreviousBlocks :: NonEmpty ((ByteString, ByteString), Block)
       withPreviousBlocks = NE.scanl1 conc blocksAndParams
   traverse mkBRID withPreviousBlocks