packages feed

morley-1.19.2: src/Morley/Tezos/Crypto/BLS.hs

-- SPDX-FileCopyrightText: 2023 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

-- | BLS-MinPk cryptographic primitives.

module Morley.Tezos.Crypto.BLS
  ( -- * Cryptographic primitive types
    PublicKey (..)
  , SecretKey
  , Signature (..)
  , detSecretKey
  , randomSecretKey
  , toPublic

  -- * Raw bytes (no checksums, tags or anything)
  , publicKeyToBytes
  , mkPublicKey
  , publicKeyLengthBytes
  , signatureToBytes
  , mkSignature
  , signatureLengthBytes

  -- * Formatting and parsing
  , formatPublicKey
  , mformatPublicKey
  , parsePublicKey
  , formatSecretKey
  , parseSecretKey
  , formatSignature
  , mformatSignature
  , parseSignature

  -- * Signing
  , sign
  , checkSignature
  ) where

import Crypto.BLST qualified as BLST
import Crypto.Random (MonadRandom(getRandomBytes))
import Data.ByteArray (ByteArray, ByteArrayAccess, Bytes, convert)
import Data.ByteArray.Sized (SizedByteArray(..), sizedByteArray)
import Fmt (Buildable, build)

import Morley.Michelson.Text
import Morley.Tezos.Crypto.Util

----------------------------------------------------------------------------
-- Types, instances, conversions
----------------------------------------------------------------------------

type MinPk = 'BLST.G1

-- | Domain separation tag used by tezos
dst :: Maybe ByteString
-- see https://gitlab.com/nomadic-labs/cryptography/ocaml-bls12-381-signature/-/blob/1.0.0/src/bls12_381_signature.ml#L350
-- https://gitlab.com/tezos/tezos/-/blob/e9a9b61969b7a44749bed9bd9cdbcb4f2283220f/src/lib_crypto/bls.ml#L333
dst = Just "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_"

-- | BLS-MinPk public cryptographic key.
newtype PublicKey = PublicKey
  { unPublicKey :: BLST.PublicKey MinPk
  } deriving stock (Show, Eq, Generic)
    deriving anyclass NFData

instance Ord PublicKey where
  compare = compare `on` (publicKeyToBytes :: PublicKey -> ByteString)

-- | BLS-MinPk secret cryptographic key.
newtype SecretKey = SecretKey
  { unSecretKey :: BLST.SecretKey
  } deriving stock (Show, Eq, Generic)
    deriving anyclass NFData

-- | Deterministicaly generate a secret key from seed.
detSecretKey :: ByteString -> SecretKey
detSecretKey seed = SecretKey $ deterministic seed do
  bs <- fromMaybe (error "impossible") . sizedByteArray @32 @Bytes <$> getRandomBytes 32
  pure $ BLST.keygen bs

-- | Generate a random secret key.
randomSecretKey :: MonadRandom m => m SecretKey
randomSecretKey = detSecretKey <$> getRandomBytes 32

-- | Create a public key from a secret key.
toPublic :: SecretKey -> PublicKey
toPublic = PublicKey . BLST.skToPk . unSecretKey

-- | BLS-MinPk cryptographic signature.
newtype Signature = Signature
  { unSignature :: BLST.Signature MinPk 'BLST.Hash
  } deriving stock (Show, Eq, Generic)
    deriving anyclass NFData

----------------------------------------------------------------------------
-- Conversion to/from raw bytes (no checksums, tags or anything)
----------------------------------------------------------------------------

-- | Convert a 'PublicKey' to raw bytes.
publicKeyToBytes :: ByteArray ba => PublicKey -> ba
publicKeyToBytes = convert . unSizedByteArray . BLST.compressPk . unPublicKey

-- | Convert a 'PublicKey' to raw bytes.
secretKeyToBytes :: ByteArray ba => SecretKey -> ba
secretKeyToBytes = convert . unSizedByteArray . BLST.serializeSk . unSecretKey

toSized
  :: forall n a. (KnownNat n, ByteArrayAccess a)
  => LText -> a -> Either CryptoParseError (SizedByteArray n a)
toSized what =
  maybeToRight (CryptoParseUnexpectedLength what $ fromIntegralOverflowing $ natVal @n Proxy)
  . sizedByteArray

-- | Make a 'PublicKey' from raw bytes.
mkPublicKey :: ByteArrayAccess ba => ba -> Either CryptoParseError PublicKey
mkPublicKey = toSized "public key" >=> bimap CryptoParseBLSTError PublicKey . BLST.decompressPk

publicKeyLengthBytes :: (Integral n, CheckIntSubType Int n) => n
publicKeyLengthBytes = fromIntegral $ BLST.byteSize @'BLST.Compress @(BLST.PublicKey MinPk)

-- | Convert a 'Signature' to raw bytes.
signatureToBytes :: ByteArray ba => Signature -> ba
signatureToBytes = convert . unSizedByteArray . BLST.compressSignature . unSignature

-- | Make a 'Signature' from raw bytes.
mkSignature :: ByteArrayAccess ba => ba -> Either CryptoParseError Signature
mkSignature = toSized "signature" >=>
  bimap CryptoParseBLSTError Signature . BLST.decompressSignature

signatureLengthBytes :: (Integral n, CheckIntSubType Int n) => n
signatureLengthBytes = fromIntegral $ BLST.byteSize @'BLST.Compress @(BLST.Signature MinPk 'BLST.Hash)

mkSecretKey :: ByteArrayAccess ba => ba -> Either CryptoParseError SecretKey
mkSecretKey = toSized "secret key" >=>
  bimap CryptoParseBLSTError SecretKey . pure . BLST.deserializeSk

----------------------------------------------------------------------------
-- Magic bytes
----------------------------------------------------------------------------

-- https://gitlab.com/tezos/tezos/-/blob/4b0dd9e9715ce82ac6429571d8843ab681522daf/src/lib_crypto/base58.ml#L428

publicKeyTag :: ByteString
publicKeyTag = "\006\149\135\204" -- BLpk(76)

secretKeyTag :: ByteString
secretKeyTag = "\003\150\192\040" -- BLsk(54)

signatureTag :: ByteString
signatureTag = "\040\171\064\207" -- BLsig(142)

----------------------------------------------------------------------------
-- Formatting
----------------------------------------------------------------------------

formatPublicKey :: PublicKey -> Text
formatPublicKey = formatImpl publicKeyTag . publicKeyToBytes @Bytes

mformatPublicKey :: PublicKey -> MText
mformatPublicKey = unsafe . mkMText . formatPublicKey

instance Buildable PublicKey where
  build = build . formatPublicKey

parsePublicKey :: Text -> Either CryptoParseError PublicKey
parsePublicKey = parseImpl publicKeyTag mkPublicKey

formatSecretKey :: SecretKey -> Text
formatSecretKey = formatImpl secretKeyTag . secretKeyToBytes @Bytes

instance Buildable SecretKey where
  build = build . formatSecretKey

parseSecretKey :: Text -> Either CryptoParseError SecretKey
parseSecretKey = parseImpl secretKeyTag mkSecretKey

formatSignature :: Signature -> Text
formatSignature = formatImpl signatureTag . signatureToBytes @Bytes

mformatSignature :: Signature -> MText
mformatSignature = unsafe . mkMText . formatSignature

instance Buildable Signature where
  build = build . formatSignature

parseSignature :: Text -> Either CryptoParseError Signature
parseSignature = parseImpl signatureTag mkSignature

----------------------------------------------------------------------------
-- Signing
----------------------------------------------------------------------------

-- | Sign a message using the secret key.
sign :: SecretKey -> ByteString -> Signature
sign sk msg = Signature $ BLST.sign (unSecretKey sk)
  (publicKeyToBytes (toPublic sk) <> msg) dst
-- upstream does this concatenation with compressed public key
-- https://gitlab.com/nomadic-labs/cryptography/ocaml-bls12-381-signature/-/blob/531ed1f509a974f5067f431b6797b9246518520c/src/bls12_381_signature.ml#L662

-- | Check that a sequence of bytes has been signed with a given key.
checkSignature :: PublicKey -> Signature -> ByteString -> Bool
checkSignature pkg@(PublicKey pk) (Signature sig) bytes =
  BLST.BlstSuccess == BLST.verify sig pk (publicKeyToBytes pkg <> bytes) dst