packages feed

biscuit-haskell-0.5.0.0: src/Auth/Biscuit/Crypto.hs

{-# LANGUAGE CPP                        #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TupleSections              #-}
{-# LANGUAGE TypeApplications           #-}
{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
{-|
  Module      : Auth.Biscuit.Crypto
  Copyright   : © Clément Delafargue, 2021
  License     : BSD-3-Clause
  Maintainer  : clement@delafargue.name
  Cryptographic helpers for biscuit signatures
-}
module Auth.Biscuit.Crypto
  ( SignedBlock
  , Blocks
  , signAuthority
  , signAttenuationBlock
  , signExternalBlock
  , sign3rdPartyBlockV1
  , verifyBlocks
  , verifySecretProof
  , verifySignatureProof
  , getSignatureProof
  , verifyExternalSigV1
  , PublicKey
  , pkBytes
  , readEd25519PublicKey
  , SecretKey
  , skBytes
  , readEd25519SecretKey
  , Signature
  , sigBytes
  , signature
  , generateSecretKey
  , toPublic
  , sign
  ) where

import           Control.Arrow              ((&&&))
import           Crypto.Error               (maybeCryptoError)
import qualified Crypto.PubKey.Ed25519      as Ed25519
import           Data.ByteArray             (convert)
import           Data.ByteString            (ByteString)
import           Data.Function              (on)
import           Data.Int                   (Int32)
import           Data.List.NonEmpty         (NonEmpty (..))
import           Data.Maybe                 (fromJust, fromMaybe, isJust)
import           Instances.TH.Lift          ()
import           Language.Haskell.TH.Syntax

import qualified Auth.Biscuit.Proto         as PB
import qualified Data.Serialize             as PB

newtype PublicKey = PublicKey Ed25519.PublicKey
  deriving newtype (Eq, Show)

instance Ord PublicKey where
  compare = compare `on` serializePublicKey

instance Lift PublicKey where
  lift pk = [| fromJust $ readEd25519PublicKey $(lift $ pkBytes pk) |]
#if MIN_VERSION_template_haskell(2,17,0)
  liftTyped = liftCode . unsafeTExpCoerce . lift
#else
  liftTyped = unsafeTExpCoerce . lift
#endif

newtype SecretKey = SecretKey Ed25519.SecretKey
  deriving newtype (Eq, Show)
newtype Signature = Signature ByteString
  deriving newtype (Eq, Show)

signature :: ByteString -> Signature
signature = Signature

sigBytes :: Signature -> ByteString
sigBytes (Signature b) = b

readEd25519PublicKey :: ByteString -> Maybe PublicKey
readEd25519PublicKey bs = PublicKey <$> maybeCryptoError (Ed25519.publicKey bs)

readEd25519SecretKey :: ByteString -> Maybe SecretKey
readEd25519SecretKey bs = SecretKey <$> maybeCryptoError (Ed25519.secretKey bs)

readEd25519Signature :: Signature -> Maybe Ed25519.Signature
readEd25519Signature (Signature bs) = maybeCryptoError (Ed25519.signature bs)

-- | Generate a public key from a secret key
toPublic :: SecretKey -> PublicKey
toPublic (SecretKey sk) = PublicKey $ Ed25519.toPublic sk

generateSecretKey :: IO SecretKey
generateSecretKey = SecretKey <$> Ed25519.generateSecretKey

sign :: SecretKey -> PublicKey -> ByteString -> Signature
sign (SecretKey sk) (PublicKey pk) payload =
  Signature . convert $ Ed25519.sign sk pk payload

verify :: PublicKey -> ByteString -> Signature -> Bool
verify (PublicKey pk) payload sig =
  case readEd25519Signature sig of
    Just sig' -> Ed25519.verify pk payload sig'
    Nothing   -> False

pkBytes :: PublicKey -> ByteString
pkBytes (PublicKey pk) = convert pk

skBytes :: SecretKey -> ByteString
skBytes (SecretKey sk) = convert sk

type SignedBlock =
  ( ByteString -- payload
  , Signature -- signature
  , PublicKey -- nextKey
  , Maybe (Signature, PublicKey) -- externalKey
  , Maybe Int -- version
  )
type Blocks = NonEmpty SignedBlock

type AnySignedBlock a =
  ( ByteString -- payload
  , a
  , PublicKey -- nextKey
  , Maybe (Signature, PublicKey) -- externalKey
  , Maybe Int -- version
  )
-- | Biscuit 2.0 allows multiple signature algorithms.
-- For now this lib only supports Ed25519, but the spec mandates flagging
-- each publicKey with an algorithm identifier when serializing it. The
-- serializing itself is handled by protobuf, but we still need to manually
-- serialize keys when we include them in something we want sign (block
-- signatures, and the final signature for sealed tokens).
serializePublicKey :: PublicKey -> ByteString
serializePublicKey pk =
  let keyBytes = pkBytes pk
      algId :: Int32
      algId = fromIntegral $ fromEnum PB.Ed25519
      -- The spec mandates that we serialize the algorithm id as a little-endian int32
      algBytes = PB.runPut $ PB.putInt32le algId
   in algBytes <> keyBytes

signBlockV0 :: SecretKey
            -> ByteString
            -> Maybe (Signature, PublicKey)
            -> IO (SignedBlock, SecretKey)
signBlockV0 sk payload eSig = do
  let pk = toPublic sk
  (nextPk, nextSk) <- (toPublic &&& id) <$> generateSecretKey
  let toSign = getSignaturePayloadV0 (payload, (), nextPk, eSig, Nothing)
      sig = sign sk pk toSign
  pure ((payload, sig, nextPk, eSig, Nothing), nextSk)

signExternalBlockV0 :: SecretKey
                    -> SecretKey
                    -> PublicKey
                    -> ByteString
                    -> IO (SignedBlock, SecretKey)
signExternalBlockV0 sk eSk pk payload =
  let eSig = sign3rdPartyBlockV0 eSk pk payload
   in signBlockV0 sk payload (Just eSig)

sign3rdPartyBlockV0 :: SecretKey
                    -> PublicKey
                    -> ByteString
                    -> (Signature, PublicKey)
sign3rdPartyBlockV0 eSk nextPk payload =
  let toSign = payload <> serializePublicKey nextPk
      ePk = toPublic eSk
      eSig = sign eSk ePk toSign
   in (eSig, ePk)

getSignatureProof :: SignedBlock -> SecretKey -> Signature
getSignatureProof (lastPayload, Signature lastSig, lastPk, _, _) nextSecret =
  let sk = nextSecret
      pk = toPublic nextSecret
      toSign = lastPayload <> serializePublicKey lastPk <> lastSig
   in sign sk pk toSign

getSignaturePayloadV0 :: AnySignedBlock a -> ByteString
getSignaturePayloadV0 (p, _, nextPk, ePk, _) =
  p <> foldMap (sigBytes . fst) ePk <> serializePublicKey nextPk

-- | The data signed by the external key is the payload for the current block + the public key from
-- the previous block: this prevents signature reuse (the external signature cannot be used on another
-- token)
getExternalSignaturePayloadV0 :: PublicKey -> SignedBlock -> Maybe (PublicKey, ByteString, Signature)
getExternalSignaturePayloadV0 pkN (payload, _, _, Just (eSig, ePk), _) = Just (ePk, payload <> serializePublicKey pkN, eSig)
getExternalSignaturePayloadV0 _ _ = Nothing

getAuthoritySignaturePayloadV1 :: ByteString -> PublicKey -> ByteString
getAuthoritySignaturePayloadV1 p nextPk =
  "\0BLOCK\0" <>
  "\0VERSION\0" <> PB.runPut (PB.putInt32le 1) <>
  "\0PAYLOAD\0" <> p <>
  serializePublicKeyV1 nextPk

getBlockSignaturePayloadV1 :: ByteString -> PublicKey -> Maybe (Signature, PublicKey) -> Signature -> ByteString
getBlockSignaturePayloadV1 p nextPk ePk prevSig =
  getAuthoritySignaturePayloadV1 p nextPk <>
    "\0PREVSIG\0" <> sigBytes prevSig <>
    foldMap serializeExternalSignatureV1 ePk

getExternalSignaturePayloadV1 :: ByteString -> Signature -> ByteString
getExternalSignaturePayloadV1 payload prevSig =
  "\0EXTERNAL\0" <>
  "\0VERSION\0" <> PB.runPut (PB.putInt32le 1) <>
  "\0PAYLOAD\0" <> payload <>
  "\0PREVSIG\0" <> sigBytes prevSig

serializePublicKeyV1 :: PublicKey -> ByteString
serializePublicKeyV1 pk =
  let keyBytes = pkBytes pk
      algId :: Int32
      algId = fromIntegral $ fromEnum PB.Ed25519
      -- The spec mandates that we serialize the algorithm id as a little-endian int32
      algBytes = PB.runPut $ PB.putInt32le algId
   in "\0ALGORITHM\0" <> algBytes <>
      "\0NEXTKEY\0" <> keyBytes

serializeExternalSignatureV1 :: (Signature, PublicKey) -> ByteString
serializeExternalSignatureV1 (sig, _) = "\0EXTERNALSIG\0" <> sigBytes sig

getSignature :: SignedBlock -> Signature
getSignature (_, sig, _, _, _) = sig

getPublicKey :: SignedBlock -> PublicKey
getPublicKey (_, _, pk, _, _) = pk

-- | When adding a pre-signed third-party block to a token, we make sure the third-party block is correctly
-- signed (pk-signature match, and the third-party block is pinned to the last biscuit block)
verifyExternalSigV0 :: PublicKey -> (ByteString, Signature, PublicKey) -> Bool
verifyExternalSigV0 previousPk (payload, eSig, ePk) =
  verify ePk (payload <> serializePublicKey previousPk) eSig

-- | When adding a pre-signed third-party block to a token, we make sure the third-party block is correctly
-- signed (pk-signature match, and the third-party block is pinned to the last biscuit block)
verifyExternalSigV1 :: Signature -> (ByteString, Signature, PublicKey) -> Bool
verifyExternalSigV1 prevSig (payload, eSig, ePk) =
  verify ePk (getExternalSignaturePayloadV1 payload prevSig) eSig

verifyAuthorityBlock :: SignedBlock -> PublicKey -> Bool
verifyAuthorityBlock b@(payload, sig, nextPk, _, version) rootPk =
  case fromMaybe 0 version of
    0 -> verify rootPk (getSignaturePayloadV0 b) sig
    1 -> verify rootPk (getAuthoritySignaturePayloadV1 payload nextPk) sig
    _ -> False

verifyAttenuationBlock :: SignedBlock -> SignedBlock -> Bool
verifyAttenuationBlock block previousBlock =
  let (payload, sig, nextPk, eSig', version) = block
      (_, prevSig, pk, _, _) = previousBlock
   in case (fromMaybe 0 version, eSig') of
        (0, Nothing) -> verify pk (getSignaturePayloadV0 block) sig
        (0, Just _)  -> False -- reject third-party blocks with v0 signatures
        (1, Nothing) -> verify pk (getBlockSignaturePayloadV1 payload nextPk eSig' prevSig) sig
        (1, Just (eSig, ePk)) ->
          let sv = verify pk (getBlockSignaturePayloadV1 payload nextPk eSig' prevSig) sig
              ev = verify ePk (getExternalSignaturePayloadV1 payload prevSig) eSig
           in sv && ev
        _          -> False

verifyBlocks :: Blocks
             -> PublicKey
             -> Bool
verifyBlocks (authority :| attenuationBlocks) rootPk =
  let attenuationBlocks' = zip attenuationBlocks (authority : attenuationBlocks)
   in verifyAuthorityBlock authority rootPk
  && all (uncurry verifyAttenuationBlock) attenuationBlocks'

verifySecretProof :: SecretKey
                  -> SignedBlock
                  -> Bool
verifySecretProof nextSecret (_, _, lastPk, _, _) =
  lastPk == toPublic nextSecret


verifySignatureProof :: Signature
                     -> SignedBlock
                     -> Bool
verifySignatureProof extraSig (lastPayload, Signature lastSig, lastPk, _, _) =
  let toSign = lastPayload <> serializePublicKey lastPk <> lastSig
   in verify lastPk toSign extraSig

signAuthorityBlockV1 :: SecretKey -> ByteString -> IO (SignedBlock, SecretKey)
signAuthorityBlockV1 sk payload = do
  let pk = toPublic sk
  (nextPk, nextSk) <- (toPublic &&& id) <$> generateSecretKey
  let toSign = getAuthoritySignaturePayloadV1 payload nextPk
      sig = sign sk pk toSign
  pure ((payload, sig, nextPk, Nothing, Just 1), nextSk)

signAttenuationBlockV1 :: SecretKey -> Signature -> ByteString -> Maybe (Signature, PublicKey) -> IO (SignedBlock, SecretKey)
signAttenuationBlockV1 sk prevSig payload ePk = do
  let pk = toPublic sk
  (nextPk, nextSk) <- (toPublic &&& id) <$> generateSecretKey
  let toSign = getBlockSignaturePayloadV1 payload nextPk ePk prevSig
      sig = sign sk pk toSign
  pure ((payload, sig, nextPk, ePk, Just 1), nextSk)

sign3rdPartyBlockV1 :: SecretKey
                    -> Signature
                    -> ByteString
                    -> (Signature, PublicKey)
sign3rdPartyBlockV1 eSk prevSig payload =
  let toSign = getExternalSignaturePayloadV1 payload prevSig
      ePk = toPublic eSk
      eSig = sign eSk ePk toSign
   in (eSig, ePk)

signAuthority :: SecretKey
              -> (ByteString, Int)
              -> IO (SignedBlock, SecretKey)
signAuthority secretKey (payload, blockVersion)
  | blockVersion >= 6 = signAuthorityBlockV1 secretKey payload
  | otherwise = signBlockV0 secretKey payload Nothing

signAttenuationBlock :: SecretKey
                     -> Signature
                     -> (ByteString, Int)
                     -> Maybe (Signature, PublicKey)
                     -> IO (SignedBlock, SecretKey)
signAttenuationBlock secretKey prevSig (payload, blockVersion) ePk
  | blockVersion >= 6 || isJust ePk = signAttenuationBlockV1 secretKey prevSig payload ePk
  | otherwise = signBlockV0 secretKey payload ePk

signExternalBlock :: SecretKey
                  -> Signature
                  -> (ByteString, Int)
                  -> SecretKey
                  -> IO (SignedBlock, SecretKey)
signExternalBlock secretKey prevSig (payload, blockVersion) eSk =
   let ePk = sign3rdPartyBlockV1 eSk prevSig payload
    in signAttenuationBlock secretKey prevSig (payload, blockVersion) (Just ePk)