-- | Inbound pubsub message validation (specs/pubsub/README.md).
--
-- Under @StrictSign@ the consuming side must enforce that @from@, @seqno@ and
-- @signature@ are present, that the signature verifies against the author's
-- public key, and that the key matches the @from@ peer ID. Messages that fail
-- are dropped without propagation. Under @StrictNoSign@ the signing fields must
-- be absent.
--
-- The signed bytes are @"libp2p-pubsub:" <> protobuf(msg without signature and
-- key)@, symmetric with the signing path in "LibP2P.Protocol.GossipSub.Router".
module LibP2P.Protocol.GossipSub.Validation
  ( ValidationError (..)
  , validateMessage
  , signaturePrefix
  , marshalForSigning
  , signingBytes
  ) where

import Prelude
import Control.Monad (unless, when)
import Data.ByteString (ByteString)
import LibP2P.Core.Multihash (HashFunction (..), validateMultihash)
import LibP2P.Crypto.Key (PublicKey, verify)
import LibP2P.Crypto.PeerId (PeerId (..), fromPublicKey)
import LibP2P.Crypto.Protobuf (decodePublicKey)
import LibP2P.Protocol.GossipSub.Message (encodePubSubMessageBS)
import LibP2P.Protocol.GossipSub.Types

-- | Why an inbound message was rejected.
data ValidationError
  = MissingField String       -- ^ A field required by StrictSign is absent
  | UnexpectedField String    -- ^ A signing field is present under StrictNoSign
  | MalformedKey String       -- ^ The key field (or inlined key) failed to decode
  | KeyPeerIdMismatch         -- ^ The public key does not derive the @from@ peer ID
  | BadSignature              -- ^ Signature verification failed
  deriving (Int -> ValidationError -> ShowS
[ValidationError] -> ShowS
ValidationError -> String
(Int -> ValidationError -> ShowS)
-> (ValidationError -> String)
-> ([ValidationError] -> ShowS)
-> Show ValidationError
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ValidationError -> ShowS
showsPrec :: Int -> ValidationError -> ShowS
$cshow :: ValidationError -> String
show :: ValidationError -> String
$cshowList :: [ValidationError] -> ShowS
showList :: [ValidationError] -> ShowS
Show, ValidationError -> ValidationError -> Bool
(ValidationError -> ValidationError -> Bool)
-> (ValidationError -> ValidationError -> Bool)
-> Eq ValidationError
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ValidationError -> ValidationError -> Bool
== :: ValidationError -> ValidationError -> Bool
$c/= :: ValidationError -> ValidationError -> Bool
/= :: ValidationError -> ValidationError -> Bool
Eq)

-- | Domain separation prefix for pubsub message signatures.
signaturePrefix :: ByteString
signaturePrefix :: ByteString
signaturePrefix = ByteString
"libp2p-pubsub:"

-- | Marshal a message for signature computation.
-- Per the libp2p spec, the signed data excludes both signature and key fields.
marshalForSigning :: PubSubMessage -> ByteString
marshalForSigning :: PubSubMessage -> ByteString
marshalForSigning PubSubMessage
msg = PubSubMessage -> ByteString
encodePubSubMessageBS
  (PubSubMessage
msg { msgSignature = Nothing, msgKey = Nothing })

-- | The exact bytes covered by a message signature.
signingBytes :: PubSubMessage -> ByteString
signingBytes :: PubSubMessage -> ByteString
signingBytes PubSubMessage
msg = ByteString
signaturePrefix ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> PubSubMessage -> ByteString
marshalForSigning PubSubMessage
msg

-- | Validate an inbound message against the configured signature policy.
validateMessage :: SignaturePolicy -> PubSubMessage -> Either ValidationError ()
validateMessage :: SignaturePolicy -> PubSubMessage -> Either ValidationError ()
validateMessage SignaturePolicy
StrictSign PubSubMessage
msg = do
  from <- String -> Maybe ByteString -> Either ValidationError ByteString
forall a. String -> Maybe a -> Either ValidationError a
required String
"from" (PubSubMessage -> Maybe ByteString
msgFrom PubSubMessage
msg)
  _    <- required "seqno" (msgSeqNo msg)
  sig  <- required "signature" (msgSignature msg)
  pk   <- authorKey from (msgKey msg)
  let PeerId derived = fromPublicKey pk
  unless (derived == from) (Left KeyPeerIdMismatch)
  unless (verify pk (signingBytes msg) sig) (Left BadSignature)
validateMessage SignaturePolicy
StrictNoSign PubSubMessage
msg = do
  String -> Maybe ByteString -> Either ValidationError ()
forall a. String -> Maybe a -> Either ValidationError ()
forbid String
"signature" (PubSubMessage -> Maybe ByteString
msgSignature PubSubMessage
msg)
  String -> Maybe ByteString -> Either ValidationError ()
forall a. String -> Maybe a -> Either ValidationError ()
forbid String
"key" (PubSubMessage -> Maybe ByteString
msgKey PubSubMessage
msg)
  String -> Maybe ByteString -> Either ValidationError ()
forall a. String -> Maybe a -> Either ValidationError ()
forbid String
"from" (PubSubMessage -> Maybe ByteString
msgFrom PubSubMessage
msg)
  String -> Maybe ByteString -> Either ValidationError ()
forall a. String -> Maybe a -> Either ValidationError ()
forbid String
"seqno" (PubSubMessage -> Maybe ByteString
msgSeqNo PubSubMessage
msg)

-- | Resolve the author's public key: from the explicit key field when present,
-- otherwise from the identity multihash inlined in the @from@ peer ID.
-- Implementations omit the key field whenever the peer ID inlines it (small
-- keys such as Ed25519), so both forms must be accepted.
authorKey :: ByteString -> Maybe ByteString -> Either ValidationError PublicKey
authorKey :: ByteString -> Maybe ByteString -> Either ValidationError PublicKey
authorKey ByteString
from Maybe ByteString
Nothing =
  case ByteString -> Either String (HashFunction, ByteString)
validateMultihash ByteString
from of
    Left String
err -> ValidationError -> Either ValidationError PublicKey
forall a b. a -> Either a b
Left (String -> ValidationError
MalformedKey (String
"from is not a valid multihash: " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> String
err))
    Right (HashFunction
SHA256, ByteString
_) -> ValidationError -> Either ValidationError PublicKey
forall a b. a -> Either a b
Left (String -> ValidationError
MissingField String
"key")
    Right (HashFunction
Identity, ByteString
inlined) -> ByteString -> Either ValidationError PublicKey
decodeKey ByteString
inlined
authorKey ByteString
_ (Just ByteString
keyBytes) = ByteString -> Either ValidationError PublicKey
decodeKey ByteString
keyBytes

decodeKey :: ByteString -> Either ValidationError PublicKey
decodeKey :: ByteString -> Either ValidationError PublicKey
decodeKey ByteString
bs = (String -> Either ValidationError PublicKey)
-> (PublicKey -> Either ValidationError PublicKey)
-> Either String PublicKey
-> Either ValidationError PublicKey
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either (ValidationError -> Either ValidationError PublicKey
forall a b. a -> Either a b
Left (ValidationError -> Either ValidationError PublicKey)
-> (String -> ValidationError)
-> String
-> Either ValidationError PublicKey
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> ValidationError
MalformedKey) PublicKey -> Either ValidationError PublicKey
forall a b. b -> Either a b
Right (ByteString -> Either String PublicKey
decodePublicKey ByteString
bs)

required :: String -> Maybe a -> Either ValidationError a
required :: forall a. String -> Maybe a -> Either ValidationError a
required String
name = Either ValidationError a
-> (a -> Either ValidationError a)
-> Maybe a
-> Either ValidationError a
forall b a. b -> (a -> b) -> Maybe a -> b
maybe (ValidationError -> Either ValidationError a
forall a b. a -> Either a b
Left (String -> ValidationError
MissingField String
name)) a -> Either ValidationError a
forall a b. b -> Either a b
Right

forbid :: String -> Maybe a -> Either ValidationError ()
forbid :: forall a. String -> Maybe a -> Either ValidationError ()
forbid String
name Maybe a
v = Bool -> Either ValidationError () -> Either ValidationError ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Bool -> (a -> Bool) -> Maybe a -> Bool
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Bool
False (Bool -> a -> Bool
forall a b. a -> b -> a
const Bool
True) Maybe a
v) (ValidationError -> Either ValidationError ()
forall a b. a -> Either a b
Left (String -> ValidationError
UnexpectedField String
name))