packages feed

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

{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns             #-}
{-# LANGUAGE RecordWildCards            #-}
{- HLINT ignore "Reduce duplication" -}
{-|
  Module      : Auth.Biscuit.Sel
  Copyright   : © Clément Delafargue, 2021
  License     : MIT
  Maintainer  : clement@delafargue.name
  Cryptographic primitives necessary to sign and verify biscuit tokens
-}
module Auth.Biscuit.Sel
  ( Keypair (..)
  , PrivateKey
  , PublicKey
  , Signature (..)
  , parsePrivateKey
  , parsePublicKey
  , serializePrivateKey
  , serializePublicKey
  , newKeypair
  , fromPrivateKey
  , signBlock
  , aggregate
  , verifySignature
  , hashBytes
  ) where

import           Control.Monad.Cont     (ContT (..), runContT)
import           Control.Monad.IO.Class (MonadIO, liftIO)
import           Data.ByteString        (ByteString, packCStringLen,
                                         useAsCStringLen)
import qualified Data.ByteString        as BS
import           Data.ByteString.Base16 as Hex
import           Data.Foldable          (for_)
import           Data.Functor           (void)
import           Data.List.NonEmpty     (NonEmpty)
import           Data.Primitive.Ptr     (copyPtr)
import           Foreign.C.Types
import           Foreign.Marshal.Alloc
import           Foreign.Ptr
import           Libsodium

-- | A private key used to generate a biscuit
newtype PrivateKey = PrivateKey ByteString
  deriving newtype (Eq, Ord)

instance Show PrivateKey where
  show (PrivateKey bs) = show $ Hex.encode bs

-- | Parse a private key from raw bytes.
-- This returns `Nothing` if the raw bytes don't have the expected length
parsePrivateKey :: ByteString -> Maybe PrivateKey
parsePrivateKey bs = if BS.length bs == cs2int crypto_core_ristretto255_scalarbytes
                     then Just (PrivateKey bs)
                     else Nothing

-- | Serialize a private key to raw bytes
serializePrivateKey :: PrivateKey -> ByteString
serializePrivateKey (PrivateKey bs) = bs

-- | A public key used to generate a biscuit
newtype PublicKey = PublicKey ByteString
  deriving newtype (Eq, Ord)

-- | Parse a public key from raw bytes.
-- This returns `Nothing` if the raw bytes don't have the expected length
parsePublicKey :: ByteString -> Maybe PublicKey
parsePublicKey bs = if BS.length bs == cs2int crypto_core_ristretto255_bytes
                     then Just (PublicKey bs)
                     else Nothing

-- | Serialize a public key to raw bytes
serializePublicKey :: PublicKey -> ByteString
serializePublicKey (PublicKey bs) = bs

instance Show PublicKey where
  show (PublicKey bs) = show $ Hex.encode bs

-- | A keypair containing both a private key and a public key
data Keypair
  = Keypair
  { privateKey :: PrivateKey
  -- ^ the private key
  , publicKey  :: PublicKey
  -- ^ the public key
  } deriving (Eq, Ord)

instance Show Keypair where
  show Keypair{privateKey, publicKey} =
    show privateKey <> "/" <> show publicKey

keypairFromScalar :: Scalar -> CIO a Keypair
keypairFromScalar scalarBuf = do
  pointBuf <- scalarToPoint scalarBuf
  privateKey <- PrivateKey <$> scalarToByteString scalarBuf
  publicKey <-  PublicKey <$> pointToByteString pointBuf
  pure Keypair{..}

-- | Generate a random keypair
newKeypair :: IO Keypair
newKeypair = runCIO $ do
  scalar <- randomScalar
  keypairFromScalar scalar

-- | Construct a keypair from a private key
fromPrivateKey :: PrivateKey -> IO Keypair
fromPrivateKey (PrivateKey privBs) = runCIO $ do
  (privBuf, _) <- withBSLen privBs
  keypairFromScalar privBuf

-- | The signature of a series of blocks (raw bytestrings)
data Signature
  = Signature
  { parameters :: [ByteString]
  -- ^ the list of parameters used to sign each block
  , z          :: ByteString
  -- ^ the aggregated signature
  } deriving (Eq, Show)

type Scalar = Ptr CUChar
type Point = Ptr CUChar

-- | Pointer allocations are written in a continuation passing style,
-- this type alias allows to use monadic notation instead of nesting
-- callbacks
type CIO a = ContT a IO

-- | Run a continuation to get back an IO value.
runCIO :: ContT a IO a -> IO a
runCIO = (`runContT` pure)

voidIO :: IO a -> CIO b ()
voidIO = void . liftIO

scalarToByteString :: MonadIO m => Ptr CUChar -> m ByteString
scalarToByteString ptr =
  let scalarIntSize = cs2int crypto_core_ristretto255_scalarbytes
   in liftIO $ packCStringLen (castPtr ptr, scalarIntSize)

pointToByteString :: MonadIO m => Ptr CUChar -> m ByteString
pointToByteString ptr =
  let pointIntSize = cs2int crypto_core_ristretto255_bytes
   in liftIO $ packCStringLen (castPtr ptr, pointIntSize)

randomScalar :: CIO a Scalar
randomScalar = do
  scalarBuf <- withScalar
  liftIO $ crypto_core_ristretto255_scalar_random scalarBuf
  pure scalarBuf

withScalar :: CIO a Scalar
withScalar =
  let intScalarSize = cs2int crypto_core_ristretto255_scalarbytes
   in ContT $ allocaBytes intScalarSize

withPoint :: CIO a Point
withPoint =
  let intPointSize = cs2int crypto_core_ristretto255_bytes
   in ContT $ allocaBytes intPointSize

scalarToPoint :: Scalar
              -> CIO a Point
scalarToPoint scalar = do
  pointBuf <- withPoint
  voidIO $ crypto_scalarmult_ristretto255_base pointBuf scalar
  pure pointBuf

withBSLen :: ByteString
          -> ContT a IO (Ptr CUChar, CULLong)
withBSLen bs = do
  (buf, int) <- ContT $ useAsCStringLen bs
  pure (castPtr buf, toEnum int)

scalarAdd :: Scalar -> Scalar -> CIO a Scalar
scalarAdd x y = do
  z <- withScalar
  liftIO $ crypto_core_ristretto255_scalar_add z x y
  pure z

scalarAddBs :: ByteString -> ByteString -> CIO a ByteString
scalarAddBs xBs yBs = do
  (x, _) <- withBSLen xBs
  (y, _) <- withBSLen yBs
  z <- scalarAdd x y
  scalarToByteString z

scalarMul :: Scalar -> Scalar -> CIO a Scalar
scalarMul x y = do
  z <- withScalar
  z <$ liftIO (crypto_core_ristretto255_scalar_mul z x y)

scalarSub :: Scalar -> Scalar -> CIO a Scalar
scalarSub x y = do
  z <- withScalar
  z <$ liftIO (crypto_core_ristretto255_scalar_sub z x y)

scalarReduce :: Ptr CUChar -> CIO a Scalar
scalarReduce bytes = do
  z <- withScalar
  z <$ liftIO (crypto_core_ristretto255_scalar_reduce z bytes)

scalarMulPoint :: Scalar -> Point -> CIO a Point
scalarMulPoint p q = do
  n <- withScalar
  n <$ liftIO (crypto_scalarmult_ristretto255 n p q)

pointAdd :: Point -> Point -> CIO a Point
pointAdd p q = do
  r <- withPoint
  r <$ liftIO (crypto_core_ristretto255_add r p q)

pointSub :: Point -> Point -> CIO a Point
pointSub p q = do
  r <- withPoint
  r <$ liftIO (crypto_core_ristretto255_sub r p q)

zeroPoint :: CIO a Point
zeroPoint = do
  p <- withPoint
  p <$ zeroizePoint p

zeroizePoint :: MonadIO m => Point -> m ()
zeroizePoint p = liftIO $ sodium_memzero p crypto_core_ristretto255_bytes

isZeroPoint :: MonadIO m => Point -> m Bool
isZeroPoint p = liftIO $
  (== 1) <$> sodium_is_zero p crypto_core_ristretto255_scalarbytes

-- | Hash a bytestring with SHA256
hashBytes :: ByteString
          -> IO ByteString
hashBytes message = runCIO $ do
  out <- ContT $ allocaBytes $ cs2int crypto_hash_sha256_bytes
  (buf, len) <- withBSLen message
  voidIO $ crypto_hash_sha256 out buf len
  liftIO $ packCStringLen (castPtr out, cs2int crypto_hash_sha256_bytes)

hashPoint :: Point
           -> ContT a IO Scalar
hashPoint point = do
  hash   <- ContT $ allocaBytes (cs2int crypto_hash_sha512_bytes)
  voidIO $ crypto_hash_sha512 hash point (fromInteger $ toInteger crypto_core_ristretto255_bytes)
  scalarReduce hash

copyPointFrom :: Point -> Point -> IO ()
copyPointFrom to from = copyPtr to from (cs2int crypto_core_ristretto255_bytes)

hashMessage :: ByteString
            -> ByteString
            -> CIO a Scalar
hashMessage publicKey message = do
  (kpBuf, kpLen) <- withBSLen publicKey
  (msgBuf, msgLen) <- withBSLen message
  state <- liftIO crypto_hash_sha512_state'malloc
  statePtr <- ContT $ crypto_hash_sha512_state'ptr state
  voidIO $ crypto_hash_sha512_init statePtr
  voidIO $ crypto_hash_sha512_update statePtr kpBuf kpLen
  voidIO $ crypto_hash_sha512_update statePtr msgBuf msgLen
  hash <- ContT $ allocaBytes (cs2int crypto_hash_sha512_bytes)
  voidIO $ crypto_hash_sha512_final statePtr hash
  scalar <- withScalar
  voidIO $ crypto_core_ristretto255_scalar_reduce scalar hash
  pure scalar

-- | Sign a single block with the given keypair
signBlock :: Keypair -> ByteString -> IO Signature
signBlock Keypair{publicKey,privateKey} message = do
  let PublicKey pubBs = publicKey
      PrivateKey prvBs = privateKey
  (`runContT` pure) $ do
     (pk, _) <- withBSLen prvBs

     r   <- randomScalar
     aa  <- scalarToPoint r
     d   <- hashPoint aa
     e   <- hashMessage pubBs message
     rd  <- scalarMul r d
     epk <- scalarMul e pk
     z   <- scalarSub rd epk
     aaBs <- pointToByteString aa
     zBs  <- scalarToByteString z
     pure Signature { parameters = [aaBs]
                    , z = zBs
                    }

-- | Aggregate two signatures into a single one
aggregate :: Signature -> Signature -> IO Signature
aggregate first second = runCIO $ do
  z <- scalarAddBs (z first) (z second)
  pure Signature
    { parameters = parameters first <> parameters second
    , z
    }

-- | Verify a signature, given a list of messages and associated
-- public keys
verifySignature :: NonEmpty (PublicKey, ByteString)
                -> Signature
                -> IO Bool
verifySignature messagesAndPks Signature{parameters,z} = runCIO $ do
  zP      <- scalarToPoint . fst  =<< withBSLen z
  eiXiRes <- computeHashMSums messagesAndPks
  diAiRes <- computeHashPSums parameters
  resTmp  <- pointAdd zP eiXiRes
  res     <- pointSub resTmp diAiRes
  isZeroPoint res

computeHashMSums :: NonEmpty (PublicKey, ByteString)
                 -> ContT a IO Point
computeHashMSums messagesAndPks = do
  eiXiRes <- zeroPoint
  for_ messagesAndPks $ \(PublicKey publicKey, message) -> do
    ei         <- hashMessage publicKey message
    eiXi       <- scalarMulPoint ei . fst =<< withBSLen publicKey
    eiXiResTmp <- pointAdd eiXiRes eiXi
    liftIO $ copyPointFrom eiXiRes eiXiResTmp
  pure eiXiRes

computeHashPSums :: [ByteString] -- parameters
                 -> ContT a IO Point
computeHashPSums parameters = do
  diAiRes <- zeroPoint
  for_ parameters $ \aa -> do
    (aaBuf, _) <- withBSLen aa
    di         <- hashPoint aaBuf
    diAi       <- scalarMulPoint di aaBuf
    diAiResTmp <- pointAdd diAiRes diAi
    liftIO $ copyPointFrom diAiRes diAiResTmp
  pure diAiRes

cs2int :: CSize -> Int
cs2int = fromInteger . toInteger