mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-10-12 02:22:45 +00:00
review fixes
This commit is contained in:
@@ -22,8 +22,10 @@ module Simplex.Messaging.Crypto.BIP32
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import qualified Crypto.Hash as H
|
||||
import qualified Crypto.MAC.HMAC as HMAC
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.Bits (shiftR, (.&.))
|
||||
import Data.ByteString (ByteString)
|
||||
@@ -32,13 +34,14 @@ import qualified Data.ByteString.Char8 as BC
|
||||
import Data.List (intercalate)
|
||||
import Data.Word (Word32)
|
||||
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
|
||||
-- | An extended private key: the key plus its chain code.
|
||||
--
|
||||
-- 'Show' is redacting — the chain code plus one child key is enough to derive
|
||||
-- siblings, so it is secret material too.
|
||||
data ExtendedKey = ExtendedKey
|
||||
{ xkKey :: S.PrivateKey,
|
||||
{ xkKey :: S.Secp256k1PrivateKey,
|
||||
xkChainCode :: ByteString
|
||||
}
|
||||
deriving (Eq)
|
||||
@@ -88,7 +91,7 @@ deriveChild xk i =
|
||||
where
|
||||
dat
|
||||
| isHardened i = B.singleton 0 <> S.unPrivateKey (xkKey xk) <> ser32 i
|
||||
| otherwise = S.serializePublicKey S.Compressed (S.publicKey (xkKey xk)) <> ser32 i
|
||||
| otherwise = S.serializePublicKey S.Compressed (S.secp256k1PublicKey (xkKey xk)) <> ser32 i
|
||||
hm = hmacSHA512 (xkChainCode xk) dat
|
||||
il = B.take 32 hm
|
||||
ir = B.drop 32 hm
|
||||
@@ -99,31 +102,19 @@ derivePath = foldl (\acc i -> acc >>= (`deriveChild` i)) . Right
|
||||
-- | Parse a path such as @m\/44'\/60'\/0'\/0\/0@. A leading @m@ or @M@ is
|
||||
-- optional; both @'@ and @h@ mark a hardened index.
|
||||
parsePath :: ByteString -> Either String [Word32]
|
||||
parsePath s = case BC.split '/' (BC.filter (/= ' ') s) of
|
||||
[] -> Right []
|
||||
(h : rest)
|
||||
| h == "m" || h == "M" || B.null h -> traverse element rest
|
||||
| otherwise -> traverse element (h : rest)
|
||||
parsePath = parseAll pathP
|
||||
|
||||
pathP :: A.Parser [Word32]
|
||||
pathP =
|
||||
optional (A.satisfy $ \c -> c == 'm' || c == 'M')
|
||||
*> ((A.char '/' *> (indexP `A.sepBy'` A.char '/')) <|> (indexP `A.sepBy1'` A.char '/') <|> pure [])
|
||||
where
|
||||
element e
|
||||
| B.null e = Left "path: empty component"
|
||||
| otherwise =
|
||||
let (digits, suffix) = BC.span (`elem` ("0123456789" :: String)) e
|
||||
mark
|
||||
| suffix == "'" || suffix == "h" || suffix == "H" = Right True
|
||||
| B.null suffix = Right False
|
||||
| otherwise = Left $ "path: bad component " <> BC.unpack e
|
||||
in if B.null digits
|
||||
then Left $ "path: bad component " <> BC.unpack e
|
||||
else do
|
||||
h' <- mark
|
||||
n <- readIndex digits
|
||||
if h' then Right (n + hardenedOffset) else Right n
|
||||
readIndex digits =
|
||||
let n = BC.foldl' (\acc c -> acc * 10 + toInteger (fromEnum c - fromEnum '0')) 0 digits
|
||||
in if n >= toInteger hardenedOffset
|
||||
then Left $ "path: index out of range: " <> BC.unpack digits
|
||||
else Right (fromInteger n)
|
||||
indexP = do
|
||||
n <- A.decimal :: A.Parser Integer
|
||||
hard <- (True <$ A.satisfy (\c -> c == '\'' || c == 'h' || c == 'H')) <|> pure False
|
||||
if n >= toInteger hardenedOffset
|
||||
then fail $ "path: index out of range: " <> show n
|
||||
else pure $ fromInteger n + if hard then hardenedOffset else 0
|
||||
|
||||
renderPath :: [Word32] -> ByteString
|
||||
renderPath is = BC.pack $ intercalate "/" ("m" : map component is)
|
||||
|
||||
@@ -33,18 +33,20 @@ import Control.Concurrent.STM
|
||||
import Crypto.Hash (Digest, SHA256, SHA512 (..), hash)
|
||||
import qualified Crypto.KDF.PBKDF2 as PBKDF2
|
||||
import Crypto.Random (ChaChaDRG, randomBytesGenerate)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Char8 as BC
|
||||
import Data.Char (toLower)
|
||||
import Data.Char (isSpace, toLower)
|
||||
import Data.IntMap.Strict (IntMap)
|
||||
import qualified Data.IntMap.Strict as IM
|
||||
import Data.List (foldl')
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Simplex.Messaging.Crypto.BIP39.English (englishWordList)
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
|
||||
-- | A validated BIP-39 mnemonic. Indexes and words are always consistent
|
||||
-- because the constructor is private and every index is in @[0, 2047]@.
|
||||
@@ -142,22 +144,25 @@ mnemonicToEntropy m = integerToBE (entBits `div` 8) (combined `shiftR` csBits)
|
||||
-- seed: 'mnemonicPhrase' always rebuilds the canonical lowercase sentence from
|
||||
-- the wordlist, and that is what 'mnemonicToSeed' hashes.
|
||||
parseMnemonic :: ByteString -> Either String Mnemonic
|
||||
parseMnemonic phrase
|
||||
| n `notElem` validWordCounts =
|
||||
Left $ "mnemonic: expected 12, 15, 18, 21 or 24 words, got " <> show n
|
||||
| otherwise = do
|
||||
parseMnemonic = parseAll mnemonicP
|
||||
|
||||
mnemonicP :: A.Parser Mnemonic
|
||||
mnemonicP = do
|
||||
ws <- A.skipSpace *> (wordP `A.sepBy1'` A.takeWhile1 isSpace) <* A.skipSpace
|
||||
let n = length ws
|
||||
if n `notElem` validWordCounts
|
||||
then fail $ "mnemonic: expected 12, 15, 18, 21 or 24 words, got " <> show n
|
||||
else do
|
||||
idxs <- traverse lookupWord ws
|
||||
let m = mnemonicFromIndexes idxs
|
||||
-- Stripping the checksum bits and recomputing them is the checksum check:
|
||||
-- if the supplied bits were wrong, the round trip cannot reproduce them.
|
||||
if entropyToIndexes (mnemonicToEntropy m) == idxs
|
||||
then Right m
|
||||
else Left "mnemonic: checksum mismatch"
|
||||
then pure m
|
||||
else fail "mnemonic: checksum mismatch"
|
||||
where
|
||||
ws = BC.words $ BC.map toLower phrase
|
||||
n = length ws
|
||||
lookupWord w =
|
||||
maybe (Left $ "mnemonic: not in wordlist: " <> BC.unpack w) Right $ M.lookup w indexByWord
|
||||
wordP = BC.map toLower <$> A.takeWhile1 A.isAlpha_ascii
|
||||
lookupWord w = maybe (fail $ "mnemonic: not in wordlist: " <> BC.unpack w) pure $ M.lookup w indexByWord
|
||||
|
||||
-- | PBKDF2-HMAC-SHA512, 2048 iterations, salt @\"mnemonic\" <> passphrase@.
|
||||
-- Pass an empty passphrase for the common case.
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
-- trust it. Note there is deliberately no normalization entry point: we never
|
||||
-- accept a foreign signature, we only produce our own.
|
||||
module Simplex.Messaging.Crypto.Secp256k1
|
||||
( PrivateKey,
|
||||
PublicKey,
|
||||
( Secp256k1PrivateKey,
|
||||
Secp256k1PublicKey,
|
||||
RecoverableSignature (..),
|
||||
PubKeyFormat (..),
|
||||
mkPrivateKey,
|
||||
unPrivateKey,
|
||||
publicKey,
|
||||
secp256k1PublicKey,
|
||||
parsePublicKey,
|
||||
serializePublicKey,
|
||||
privateKeyTweakAdd,
|
||||
@@ -43,10 +43,8 @@ where
|
||||
import Control.Monad (when)
|
||||
import Crypto.Random (drgNew, randomBytesGenerate)
|
||||
import qualified Data.ByteArray as BA
|
||||
import qualified Data.ByteArray.Encoding as BAE
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Char8 as BC
|
||||
import qualified Data.ByteString.Unsafe as BU
|
||||
import Foreign
|
||||
import Foreign.C
|
||||
@@ -85,27 +83,17 @@ compactSize = 64
|
||||
|
||||
-- | A validated secp256k1 private key: 32 bytes, in @[1, n-1]@.
|
||||
--
|
||||
-- 'Show' is redacting and 'Eq' is constant-time, both deliberately: this key
|
||||
-- authorises transfers of assets with monetary value, so it must not reach a
|
||||
-- log through a derived 'Show' and must not leak through comparison timing.
|
||||
newtype PrivateKey = PrivateKey ByteString
|
||||
-- There is no 'Show' instance, so the key cannot reach a log, and 'Eq' is
|
||||
-- constant-time: this key authorises transfers of assets with monetary value.
|
||||
newtype Secp256k1PrivateKey = Secp256k1PrivateKey ByteString
|
||||
|
||||
instance Show PrivateKey where
|
||||
show _ = "PrivateKey <redacted>"
|
||||
|
||||
instance Eq PrivateKey where
|
||||
PrivateKey a == PrivateKey b = BA.constEq a b
|
||||
instance Eq Secp256k1PrivateKey where
|
||||
Secp256k1PrivateKey a == Secp256k1PrivateKey b = BA.constEq a b
|
||||
|
||||
-- | A parsed public key, held in libsecp256k1's opaque 64-byte internal form.
|
||||
-- Use 'serializePublicKey' to get the SEC1 bytes.
|
||||
newtype PublicKey = PublicKey ByteString
|
||||
deriving newtype (Eq)
|
||||
|
||||
instance Show PublicKey where
|
||||
show pk = "PublicKey " <> BC.unpack (hex $ serializePublicKey Compressed pk)
|
||||
|
||||
hex :: ByteString -> ByteString
|
||||
hex = BAE.convertToBase BAE.Base16
|
||||
newtype Secp256k1PublicKey = Secp256k1PublicKey ByteString
|
||||
deriving newtype (Eq, Show)
|
||||
|
||||
-- | SEC1 output format for 'serializePublicKey'.
|
||||
data PubKeyFormat = Compressed | Uncompressed
|
||||
@@ -202,8 +190,8 @@ packPtr :: Ptr Word8 -> Int -> IO ByteString
|
||||
packPtr p n = B.packCStringLen (castPtr p, n)
|
||||
|
||||
-- | Marshal a 'PublicKey' back into its opaque C representation.
|
||||
withPubKeyRaw :: PublicKey -> (Ptr PubKeyRaw -> IO a) -> IO a
|
||||
withPubKeyRaw (PublicKey bs) f = withBS bs $ f . castPtr
|
||||
withPubKeyRaw :: Secp256k1PublicKey -> (Ptr PubKeyRaw -> IO a) -> IO a
|
||||
withPubKeyRaw (Secp256k1PublicKey bs) f = withBS bs $ f . castPtr
|
||||
|
||||
-- | Marshal a 'RecoverableSignature' into the opaque C representation, failing
|
||||
-- if libsecp256k1 rejects it.
|
||||
@@ -221,28 +209,28 @@ withRecSigRaw (RecoverableSignature compact recId) f
|
||||
|
||||
-- | Validate 32 bytes as a private key. Rejects zero and anything at or above
|
||||
-- the group order, which is what makes 'publicKey' and 'signRecoverable' total.
|
||||
mkPrivateKey :: ByteString -> Either String PrivateKey
|
||||
mkPrivateKey :: ByteString -> Either String Secp256k1PrivateKey
|
||||
mkPrivateKey bs
|
||||
| B.length bs /= privateKeySize = Left $ "private key: expected 32 bytes, got " <> show (B.length bs)
|
||||
| otherwise = unsafePerformIO $ withBS bs $ \p -> do
|
||||
rc <- c_ec_seckey_verify secp256k1Ctx p
|
||||
pure $ if rc == 1 then Right (PrivateKey bs) else Left "private key: not in [1, n-1]"
|
||||
pure $ if rc == 1 then Right (Secp256k1PrivateKey bs) else Left "private key: not in [1, n-1]"
|
||||
|
||||
unPrivateKey :: PrivateKey -> ByteString
|
||||
unPrivateKey (PrivateKey bs) = bs
|
||||
unPrivateKey :: Secp256k1PrivateKey -> ByteString
|
||||
unPrivateKey (Secp256k1PrivateKey bs) = bs
|
||||
|
||||
-- | Derive the public key. Total, because 'PrivateKey' is validated.
|
||||
publicKey :: PrivateKey -> PublicKey
|
||||
publicKey (PrivateKey sk) = unsafePerformIO $
|
||||
secp256k1PublicKey :: Secp256k1PrivateKey -> Secp256k1PublicKey
|
||||
secp256k1PublicKey (Secp256k1PrivateKey sk) = unsafePerformIO $
|
||||
allocaBytes pubKeyInternalSize $ \pkPtr ->
|
||||
withBS sk $ \skPtr -> do
|
||||
rc <- c_ec_pubkey_create secp256k1Ctx pkPtr skPtr
|
||||
-- Cannot fail: the key was verified by mkPrivateKey.
|
||||
when (rc /= 1) $ ioError (userError "secp256k1_ec_pubkey_create failed on a validated key")
|
||||
PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize
|
||||
Secp256k1PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize
|
||||
|
||||
-- | Parse a SEC1 point, compressed (33 bytes) or uncompressed (65 bytes).
|
||||
parsePublicKey :: ByteString -> Either String PublicKey
|
||||
parsePublicKey :: ByteString -> Either String Secp256k1PublicKey
|
||||
parsePublicKey bs
|
||||
| len /= compressedSize && len /= uncompressedSize =
|
||||
Left $ "public key: expected 33 or 65 bytes, got " <> show len
|
||||
@@ -251,12 +239,12 @@ parsePublicKey bs
|
||||
withBS bs $ \inPtr -> do
|
||||
rc <- c_ec_pubkey_parse secp256k1Ctx pkPtr inPtr (fromIntegral len)
|
||||
if rc == 1
|
||||
then Right . PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize
|
||||
then Right . Secp256k1PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize
|
||||
else pure $ Left "public key: not a valid curve point"
|
||||
where
|
||||
len = B.length bs
|
||||
|
||||
serializePublicKey :: PubKeyFormat -> PublicKey -> ByteString
|
||||
serializePublicKey :: PubKeyFormat -> Secp256k1PublicKey -> ByteString
|
||||
serializePublicKey fmt pk = unsafePerformIO $
|
||||
allocaBytes outLen $ \outPtr ->
|
||||
alloca $ \lenPtr ->
|
||||
@@ -275,8 +263,8 @@ serializePublicKey fmt pk = unsafePerformIO $
|
||||
--
|
||||
-- 'Nothing' when the result is zero or the tweak is out of range — BIP-32
|
||||
-- requires the caller to skip to the next child index in that case.
|
||||
privateKeyTweakAdd :: PrivateKey -> ByteString -> Maybe PrivateKey
|
||||
privateKeyTweakAdd (PrivateKey sk) tweak
|
||||
privateKeyTweakAdd :: Secp256k1PrivateKey -> ByteString -> Maybe Secp256k1PrivateKey
|
||||
privateKeyTweakAdd (Secp256k1PrivateKey sk) tweak
|
||||
| B.length tweak /= privateKeySize = Nothing
|
||||
| otherwise = unsafePerformIO $
|
||||
allocaBytes privateKeySize $ \skPtr ->
|
||||
@@ -284,7 +272,7 @@ privateKeyTweakAdd (PrivateKey sk) tweak
|
||||
withBS sk $ \src -> copyBytes skPtr src privateKeySize
|
||||
rc <- c_ec_seckey_tweak_add secp256k1Ctx skPtr twPtr
|
||||
if rc == 1
|
||||
then Just . PrivateKey <$> packPtr skPtr privateKeySize
|
||||
then Just . Secp256k1PrivateKey <$> packPtr skPtr privateKeySize
|
||||
else pure Nothing
|
||||
|
||||
-- | @tweak * P@. The scalar multiplication behind an ECDH shared secret.
|
||||
@@ -295,17 +283,17 @@ privateKeyTweakAdd (PrivateKey sk) tweak
|
||||
-- caller.
|
||||
--
|
||||
-- 'Nothing' when the tweak is zero or out of range.
|
||||
publicKeyTweakMul :: PublicKey -> ByteString -> Maybe PublicKey
|
||||
publicKeyTweakMul :: Secp256k1PublicKey -> ByteString -> Maybe Secp256k1PublicKey
|
||||
publicKeyTweakMul = tweakPubKey c_ec_pubkey_tweak_mul
|
||||
|
||||
-- | @P + tweak * G@, the point addition stealth address derivation needs.
|
||||
--
|
||||
-- 'Nothing' when the tweak is out of range or the result is the point at
|
||||
-- infinity.
|
||||
publicKeyTweakAdd :: PublicKey -> ByteString -> Maybe PublicKey
|
||||
publicKeyTweakAdd :: Secp256k1PublicKey -> ByteString -> Maybe Secp256k1PublicKey
|
||||
publicKeyTweakAdd = tweakPubKey c_ec_pubkey_tweak_add
|
||||
|
||||
tweakPubKey :: (Ptr Ctx -> Ptr PubKeyRaw -> Ptr Word8 -> IO CInt) -> PublicKey -> ByteString -> Maybe PublicKey
|
||||
tweakPubKey :: (Ptr Ctx -> Ptr PubKeyRaw -> Ptr Word8 -> IO CInt) -> Secp256k1PublicKey -> ByteString -> Maybe Secp256k1PublicKey
|
||||
tweakPubKey f pk tweak
|
||||
| B.length tweak /= privateKeySize = Nothing
|
||||
| otherwise = unsafePerformIO $
|
||||
@@ -314,12 +302,12 @@ tweakPubKey f pk tweak
|
||||
withPubKeyRaw pk $ \src -> copyBytes (castPtr pkPtr) (castPtr src) pubKeyInternalSize
|
||||
rc <- f secp256k1Ctx pkPtr twPtr
|
||||
if rc == 1
|
||||
then Just . PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize
|
||||
then Just . Secp256k1PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize
|
||||
else pure Nothing
|
||||
|
||||
-- | Sign a 32-byte digest. Deterministic (RFC 6979) and always low-@s@.
|
||||
signRecoverable :: PrivateKey -> ByteString -> Either String RecoverableSignature
|
||||
signRecoverable (PrivateKey sk) digest
|
||||
signRecoverable :: Secp256k1PrivateKey -> ByteString -> Either String RecoverableSignature
|
||||
signRecoverable (Secp256k1PrivateKey sk) digest
|
||||
| B.length digest /= digestSize =
|
||||
Left $ "digest: expected 32 bytes, got " <> show (B.length digest)
|
||||
| otherwise = unsafePerformIO $
|
||||
@@ -340,7 +328,7 @@ signRecoverable (PrivateKey sk) digest
|
||||
pure $ Right RecoverableSignature {rsCompact = compact, rsRecId = fromIntegral recId}
|
||||
|
||||
-- | Recover the signing public key from a signature and the digest it signed.
|
||||
recoverPublicKey :: RecoverableSignature -> ByteString -> Either String PublicKey
|
||||
recoverPublicKey :: RecoverableSignature -> ByteString -> Either String Secp256k1PublicKey
|
||||
recoverPublicKey sig digest
|
||||
| B.length digest /= digestSize =
|
||||
Left $ "digest: expected 32 bytes, got " <> show (B.length digest)
|
||||
@@ -350,7 +338,7 @@ recoverPublicKey sig digest
|
||||
withBS digest $ \msgPtr -> do
|
||||
rc <- c_ecdsa_recover secp256k1Ctx pkPtr sigPtr msgPtr
|
||||
if rc == 1
|
||||
then Right . PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize
|
||||
then Right . Secp256k1PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize
|
||||
else pure $ Left "secp256k1_ecdsa_recover failed"
|
||||
|
||||
-- | Whether @s <= n/2@, i.e. the signature is in the canonical form EIP-2
|
||||
|
||||
@@ -16,6 +16,7 @@ module Simplex.Messaging.Eth.Address
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bits (shiftR, (.&.))
|
||||
@@ -42,9 +43,21 @@ instance Show Address where
|
||||
-- Parsing accepts bare or @0x@-prefixed hex and verifies a mixed-case checksum.
|
||||
instance StrEncoding Address where
|
||||
strEncode = checksumAddress
|
||||
strP = either fail pure . parseAddress =<< A.takeWhile1 isHexOr0x
|
||||
strP = do
|
||||
_ <- optional $ A.string "0x" <|> A.string "0X"
|
||||
body <- A.takeWhile1 isHexDigit
|
||||
if B.length body /= addressSize * 2
|
||||
then fail $ "address: expected 40 hex digits, got " <> show (B.length body)
|
||||
else case fromHex (BC.map toLower body) of
|
||||
Left _ -> fail "address: not hexadecimal"
|
||||
Right bs
|
||||
| mixedCase body && checksumAddress (Address bs) /= "0x" <> body ->
|
||||
fail "address: EIP-55 checksum mismatch"
|
||||
| otherwise -> pure $ Address bs
|
||||
where
|
||||
isHexOr0x c = isHexDigit c || c == 'x' || c == 'X'
|
||||
mixedCase body = BC.any isUpper letters && BC.any isLower letters
|
||||
where
|
||||
letters = BC.filter (not . isDigit) body
|
||||
|
||||
instance ToJSON Address where
|
||||
toEncoding = strToJEncoding
|
||||
@@ -66,12 +79,12 @@ mkAddress bs
|
||||
|
||||
-- | The low 20 bytes of @keccak256@ of the uncompressed public key with its
|
||||
-- @0x04@ SEC1 prefix removed.
|
||||
addressFromPublicKey :: S.PublicKey -> Address
|
||||
addressFromPublicKey :: S.Secp256k1PublicKey -> Address
|
||||
addressFromPublicKey pk =
|
||||
Address . B.drop 12 . keccak256 . B.drop 1 $ S.serializePublicKey S.Uncompressed pk
|
||||
|
||||
addressFromPrivateKey :: S.PrivateKey -> Address
|
||||
addressFromPrivateKey = addressFromPublicKey . S.publicKey
|
||||
addressFromPrivateKey :: S.Secp256k1PrivateKey -> Address
|
||||
addressFromPrivateKey = addressFromPublicKey . S.secp256k1PublicKey
|
||||
|
||||
-- | EIP-55: @0x@ followed by 40 hex digits whose case encodes a checksum over
|
||||
-- the lowercase hex form.
|
||||
@@ -94,19 +107,7 @@ checksumAddress (Address bs) = "0x" <> B.pack (zipWith adjust [0 ..] lowerHex)
|
||||
-- its EIP-55 checksum; an all-lowercase or all-uppercase one carries no
|
||||
-- checksum and is accepted as-is, which is what every Ethereum client does.
|
||||
parseAddress :: ByteString -> Either String Address
|
||||
parseAddress s
|
||||
| B.length body /= 40 = Left $ "address: expected 40 hex digits, got " <> show (B.length body)
|
||||
| otherwise = case fromHex (BC.map toLower body) of
|
||||
Left _ -> Left "address: not hexadecimal"
|
||||
Right bs
|
||||
| mixedCase && checksumAddress (Address bs) /= "0x" <> body ->
|
||||
Left "address: EIP-55 checksum mismatch"
|
||||
| otherwise -> Right (Address bs)
|
||||
where
|
||||
body = if "0x" `B.isPrefixOf` s || "0X" `B.isPrefixOf` s then B.drop 2 s else s
|
||||
bodyC = BC.unpack body
|
||||
letters = filter (not . isDigit) bodyC
|
||||
mixedCase = any isUpper letters && any isLower letters
|
||||
parseAddress = strDecode
|
||||
|
||||
-- | BIP-44 path for Ethereum account @i@, address @k@: @m\/44'\/60'\/i'\/0\/k@.
|
||||
-- The account must be below 'hardenedOffset', as 'hardened' returns anything
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
-- > "TransferName(address from,address to,uint256 tokenId,uint256 nonce,uint256 deadline)"
|
||||
module Simplex.Messaging.Eth.EIP712
|
||||
( Eip712Domain (..),
|
||||
Value (..),
|
||||
Eip712Value (..),
|
||||
typeHash,
|
||||
encodeValue,
|
||||
encodeData,
|
||||
@@ -50,7 +50,7 @@ data Eip712Domain = Eip712Domain
|
||||
--
|
||||
-- 'VStruct' takes an already-computed 'hashStruct' result, which is how nested
|
||||
-- structs are encoded; 'VArray' hashes the concatenation of its members.
|
||||
data Value
|
||||
data Eip712Value
|
||||
= VUint Integer
|
||||
| VInt Integer
|
||||
| VBool Bool
|
||||
@@ -61,7 +61,7 @@ data Value
|
||||
VBytes ByteString
|
||||
| -- | @string@; the caller supplies UTF-8 bytes.
|
||||
VString ByteString
|
||||
| VArray [Value]
|
||||
| VArray [Eip712Value]
|
||||
| -- | A nested struct, given as its 32-byte @hashStruct@.
|
||||
VStruct ByteString
|
||||
deriving (Eq, Show)
|
||||
@@ -71,7 +71,7 @@ typeHash :: ByteString -> ByteString
|
||||
typeHash = keccak256
|
||||
|
||||
-- | Encode one member as exactly 32 bytes.
|
||||
encodeValue :: Value -> Either String ByteString
|
||||
encodeValue :: Eip712Value -> Either String ByteString
|
||||
encodeValue = \case
|
||||
VUint n
|
||||
| n < 0 || n >= two256 -> Left $ "eip712: uint256 out of range: " <> show n
|
||||
@@ -94,11 +94,11 @@ encodeValue = \case
|
||||
two256 = 2 ^ (256 :: Int) :: Integer
|
||||
two255 = 2 ^ (255 :: Int) :: Integer
|
||||
|
||||
encodeData :: [Value] -> Either String ByteString
|
||||
encodeData :: [Eip712Value] -> Either String ByteString
|
||||
encodeData vs = B.concat <$> traverse encodeValue vs
|
||||
|
||||
-- | @keccak256(typeHash ‖ encodeData(members))@.
|
||||
hashStruct :: ByteString -> [Value] -> Either String ByteString
|
||||
hashStruct :: ByteString -> [Eip712Value] -> Either String ByteString
|
||||
hashStruct typeString members = keccak256 . (typeHash typeString <>) <$> encodeData members
|
||||
|
||||
domainSeparator :: Eip712Domain -> Either String ByteString
|
||||
@@ -112,7 +112,7 @@ domainSeparator d =
|
||||
]
|
||||
|
||||
-- | The final digest to sign: @keccak256(0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct)@.
|
||||
hashTypedData :: Eip712Domain -> ByteString -> [Value] -> Either String ByteString
|
||||
hashTypedData :: Eip712Domain -> ByteString -> [Eip712Value] -> Either String ByteString
|
||||
hashTypedData d typeString members = do
|
||||
ds <- domainSeparator d
|
||||
hs <- hashStruct typeString members
|
||||
|
||||
@@ -52,8 +52,8 @@ import qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
||||
|
||||
-- | A recipient's published key pair: spending key, then viewing key.
|
||||
data StealthMetaAddress = StealthMetaAddress
|
||||
{ smaSpend :: S.PublicKey,
|
||||
smaView :: S.PublicKey
|
||||
{ smaSpend :: S.Secp256k1PublicKey,
|
||||
smaView :: S.Secp256k1PublicKey
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -74,9 +74,9 @@ data StealthDestination = StealthDestination
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
metaAddress :: S.PrivateKey -> S.PrivateKey -> StealthMetaAddress
|
||||
metaAddress :: S.Secp256k1PrivateKey -> S.Secp256k1PrivateKey -> StealthMetaAddress
|
||||
metaAddress spend view =
|
||||
StealthMetaAddress {smaSpend = S.publicKey spend, smaView = S.publicKey view}
|
||||
StealthMetaAddress {smaSpend = S.secp256k1PublicKey spend, smaView = S.secp256k1PublicKey view}
|
||||
|
||||
metaAddressSize :: Int
|
||||
metaAddressSize = 2 * S.compressedSize
|
||||
@@ -97,7 +97,7 @@ parseMetaAddress bs
|
||||
|
||||
-- | @keccak256(x || y)@ of @sk * P@ — the value both sides arrive at, the
|
||||
-- sender from the ephemeral key and the recipient from the viewing key.
|
||||
sharedSecretHash :: S.PrivateKey -> S.PublicKey -> Either String ByteString
|
||||
sharedSecretHash :: S.Secp256k1PrivateKey -> S.Secp256k1PublicKey -> Either String ByteString
|
||||
sharedSecretHash sk pk =
|
||||
case S.publicKeyTweakMul pk (S.unPrivateKey sk) of
|
||||
Nothing -> Left "stealth: shared secret is not a valid point"
|
||||
@@ -106,14 +106,14 @@ sharedSecretHash sk pk =
|
||||
-- | Sender side. @ephemeral@ must be freshly random and used once: reusing it
|
||||
-- across recipients lets them link the destinations, and reusing it for one
|
||||
-- recipient produces the same address twice.
|
||||
stealthDestination :: S.PrivateKey -> StealthMetaAddress -> Either String StealthDestination
|
||||
stealthDestination :: S.Secp256k1PrivateKey -> StealthMetaAddress -> Either String StealthDestination
|
||||
stealthDestination ephemeral ma = do
|
||||
sh <- sharedSecretHash ephemeral (smaView ma)
|
||||
stealthPub <- tweakSpend (smaSpend ma) sh
|
||||
pure
|
||||
StealthDestination
|
||||
{ sdAddress = addressFromPublicKey stealthPub,
|
||||
sdEphemeralPubKey = S.serializePublicKey S.Compressed (S.publicKey ephemeral),
|
||||
sdEphemeralPubKey = S.serializePublicKey S.Compressed (S.secp256k1PublicKey ephemeral),
|
||||
sdViewTag = B.head sh
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ stealthDestination ephemeral ma = do
|
||||
--
|
||||
-- The view tag is checked before the point addition, which is the whole reason
|
||||
-- it exists — a non-match costs one multiplication and one hash.
|
||||
stealthMatch :: S.PrivateKey -> S.PublicKey -> ByteString -> ViewTag -> Either String (Maybe Address)
|
||||
stealthMatch :: S.Secp256k1PrivateKey -> S.Secp256k1PublicKey -> ByteString -> ViewTag -> Either String (Maybe Address)
|
||||
stealthMatch view spend ephemeralPub tag = do
|
||||
eph <- S.parsePublicKey ephemeralPub
|
||||
sh <- sharedSecretHash view eph
|
||||
@@ -133,7 +133,7 @@ stealthMatch view spend ephemeralPub tag = do
|
||||
--
|
||||
-- Needs the spending key, which is why a viewing key can be delegated for
|
||||
-- scanning without granting the ability to spend.
|
||||
stealthPrivateKey :: S.PrivateKey -> S.PrivateKey -> ByteString -> Either String S.PrivateKey
|
||||
stealthPrivateKey :: S.Secp256k1PrivateKey -> S.Secp256k1PrivateKey -> ByteString -> Either String S.Secp256k1PrivateKey
|
||||
stealthPrivateKey spend view ephemeralPub = do
|
||||
eph <- S.parsePublicKey ephemeralPub
|
||||
sh <- sharedSecretHash view eph
|
||||
@@ -141,7 +141,7 @@ stealthPrivateKey spend view ephemeralPub = do
|
||||
Nothing -> Left "stealth: derived key out of range"
|
||||
Just sk -> Right sk
|
||||
|
||||
tweakSpend :: S.PublicKey -> ByteString -> Either String S.PublicKey
|
||||
tweakSpend :: S.Secp256k1PublicKey -> ByteString -> Either String S.Secp256k1PublicKey
|
||||
tweakSpend spend sh = case S.publicKeyTweakAdd spend sh of
|
||||
Nothing -> Left "stealth: derived point out of range"
|
||||
Just p -> Right p
|
||||
|
||||
@@ -76,31 +76,29 @@ secp256k1Tests = do
|
||||
it "produces low-s signatures (EIP-2)" $
|
||||
S.isLowS testSig `shouldBe` True
|
||||
it "recovers the signing key" $
|
||||
right (S.recoverPublicKey testSig testDigest) `shouldBe` S.publicKey testKey
|
||||
right (S.recoverPublicKey testSig testDigest) `shouldBe` S.secp256k1PublicKey testKey
|
||||
it "does not recover the signing key from another digest" $
|
||||
S.recoverPublicKey testSig (keccak256 "SimpleX names ") `shouldNotBe` Right (S.publicKey testKey)
|
||||
S.recoverPublicKey testSig (keccak256 "SimpleX names ") `shouldNotBe` Right (S.secp256k1PublicKey testKey)
|
||||
it "round-trips a compressed public key" $ do
|
||||
let pk = S.publicKey testKey
|
||||
let pk = S.secp256k1PublicKey testKey
|
||||
ser = S.serializePublicKey S.Compressed pk
|
||||
B.length ser `shouldBe` 33
|
||||
S.parsePublicKey ser `shouldBe` Right pk
|
||||
it "round-trips an uncompressed public key" $ do
|
||||
let pk = S.publicKey testKey
|
||||
let pk = S.secp256k1PublicKey testKey
|
||||
ser = S.serializePublicKey S.Uncompressed pk
|
||||
B.length ser `shouldBe` 65
|
||||
S.parsePublicKey ser `shouldBe` Right pk
|
||||
it "rejects a zero private key" $
|
||||
S.mkPrivateKey (B.replicate 32 0) `shouldSatisfy` isLeft
|
||||
isLeft (S.mkPrivateKey (B.replicate 32 0)) `shouldBe` True
|
||||
it "rejects a private key at the group order" $
|
||||
S.mkPrivateKey (hx "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141") `shouldSatisfy` isLeft
|
||||
isLeft (S.mkPrivateKey (hx "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")) `shouldBe` True
|
||||
it "rejects a short private key" $
|
||||
S.mkPrivateKey (B.replicate 31 1) `shouldSatisfy` isLeft
|
||||
isLeft (S.mkPrivateKey (B.replicate 31 1)) `shouldBe` True
|
||||
it "rejects a digest that is not 32 bytes" $
|
||||
S.signRecoverable testKey (B.replicate 31 0) `shouldSatisfy` isLeft
|
||||
it "rejects a malformed public key" $
|
||||
S.parsePublicKey (B.replicate 33 0) `shouldSatisfy` isLeft
|
||||
it "redacts the private key in Show" $
|
||||
show testKey `shouldBe` "PrivateKey <redacted>"
|
||||
it "adds a tweak to a private key" $
|
||||
(toHex . S.unPrivateKey <$> S.privateKeyTweakAdd testKey (B.replicate 31 0 <> B.singleton 1))
|
||||
`shouldBe` Just "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362319"
|
||||
@@ -506,7 +504,7 @@ stealthTests = do
|
||||
it "rejects points not on the curve" $
|
||||
parseMetaAddress (B.replicate 66 0xAA) `shouldSatisfy` isLeft
|
||||
|
||||
aliceSpend, aliceView, bobSpend, bobView, ephemeralKey, ephemeralKey2 :: S.PrivateKey
|
||||
aliceSpend, aliceView, bobSpend, bobView, ephemeralKey, ephemeralKey2 :: S.Secp256k1PrivateKey
|
||||
aliceSpend = right $ S.mkPrivateKey (hx "1111111111111111111111111111111111111111111111111111111111111111")
|
||||
aliceView = right $ S.mkPrivateKey (hx "2222222222222222222222222222222222222222222222222222222222222222")
|
||||
bobSpend = right $ S.mkPrivateKey (hx "3333333333333333333333333333333333333333333333333333333333333333")
|
||||
@@ -519,5 +517,5 @@ aliceMeta = metaAddress aliceSpend aliceView
|
||||
bobMeta = metaAddress bobSpend bobView
|
||||
|
||||
-- Distinct ephemeral keys for batch tests.
|
||||
ephemeralN :: Int -> S.PrivateKey
|
||||
ephemeralN :: Int -> S.Secp256k1PrivateKey
|
||||
ephemeralN i = right . S.mkPrivateKey . keccak256 . BC.pack $ "ephemeral " <> show i
|
||||
|
||||
Reference in New Issue
Block a user