biscuit-haskell-0.3.0.0: src/Auth/Biscuit/Crypto.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
module Auth.Biscuit.Crypto
( SignedBlock
, Blocks
, signBlock
, signExternalBlock
, sign3rdPartyBlock
, verifyBlocks
, verifySecretProof
, verifySignatureProof
, getSignatureProof
, verifyExternalSig
, 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 qualified Data.List.NonEmpty as NE
import Data.Maybe (catMaybes, fromJust)
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, Signature, PublicKey, Maybe (Signature, PublicKey))
type Blocks = NonEmpty SignedBlock
-- | 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
signBlock :: SecretKey
-> ByteString
-> Maybe (Signature, PublicKey)
-> IO (SignedBlock, SecretKey)
signBlock sk payload eSig = do
let pk = toPublic sk
(nextPk, nextSk) <- (toPublic &&& id) <$> generateSecretKey
let toSign = getToSig (payload, (), nextPk, eSig)
sig = sign sk pk toSign
pure ((payload, sig, nextPk, eSig), nextSk)
signExternalBlock :: SecretKey
-> SecretKey
-> PublicKey
-> ByteString
-> IO (SignedBlock, SecretKey)
signExternalBlock sk eSk pk payload =
let eSig = sign3rdPartyBlock eSk pk payload
in signBlock sk payload (Just eSig)
sign3rdPartyBlock :: SecretKey
-> PublicKey
-> ByteString
-> (Signature, PublicKey)
sign3rdPartyBlock 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, _todo) nextSecret =
let sk = nextSecret
pk = toPublic nextSecret
toSign = lastPayload <> serializePublicKey lastPk <> lastSig
in sign sk pk toSign
getToSig :: (ByteString, a, PublicKey, Maybe (Signature, PublicKey)) -> ByteString
getToSig (p, _, nextPk, ePk) =
p <> foldMap (sigBytes . fst) ePk <> serializePublicKey nextPk
getSignature :: SignedBlock -> Signature
getSignature (_, sig, _, _) = sig
getPublicKey :: SignedBlock -> PublicKey
getPublicKey (_, _, pk, _) = pk
-- | 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)
getExternalSigPayload :: PublicKey -> SignedBlock -> Maybe (PublicKey, ByteString, Signature)
getExternalSigPayload pkN (payload, _, _, Just (eSig, ePk)) = Just (ePk, payload <> serializePublicKey pkN, eSig)
getExternalSigPayload _ _ = Nothing
-- | 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)
verifyExternalSig :: PublicKey -> (ByteString, Signature, PublicKey) -> Bool
verifyExternalSig previousPk (payload, eSig, ePk) =
verify ePk (payload <> serializePublicKey previousPk) eSig
verifyBlocks :: Blocks
-> PublicKey
-> Bool
verifyBlocks blocks rootPk =
let attachKey pk (payload, sig) = (pk, payload, sig)
uncurry3 f (a, b, c) = f a b c
sigs = getSignature <$> blocks
toSigs = getToSig <$> blocks
-- key for block 0 is the root key
-- key for block n is the key from block (n - 1)
keys = pure rootPk <> (getPublicKey <$> blocks)
keysPayloadsSigs = NE.zipWith attachKey keys (NE.zip toSigs sigs)
-- external_signature(block_n) = sign(external_key_n, payload_n <> public_key_n-1)
-- so we need to pair each block with the public key carried by the previous block
-- (the authority block can't have an external signature)
previousKeys = getPublicKey <$> NE.init blocks
blocksAfterAuthority = NE.tail blocks
eKeysPayloadsESigs = catMaybes $ zipWith getExternalSigPayload previousKeys blocksAfterAuthority
in all (uncurry3 verify) keysPayloadsSigs
&& all (uncurry3 verify) eKeysPayloadsESigs
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