review fixes

This commit is contained in:
Alain Brenzikofer
2026-09-20 15:28:49 +02:00
parent ae7a312af7
commit 86ac7fdad0
7 changed files with 111 additions and 128 deletions
+17 -26
View File
@@ -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)
+16 -11
View File
@@ -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.
+33 -45
View File
@@ -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
+19 -18
View File
@@ -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
+7 -7
View File
@@ -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
+10 -10
View File
@@ -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
+9 -11
View File
@@ -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