diff --git a/scripts/format b/scripts/format index 21b7231..6a4f57a 100755 --- a/scripts/format +++ b/scripts/format @@ -4,3 +4,5 @@ REPO_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd ) find $REPO_DIR/secp256k1-haskell/src -type f -name "*.hs" | xargs ormolu -i find $REPO_DIR/secp256k1-haskell/test -type f -name "*.hs" | xargs ormolu -i +find $REPO_DIR/secp256k1-haskell-recovery/src -type f -name "*.hs" | xargs ormolu -i +find $REPO_DIR/secp256k1-haskell-recovery/test -type f -name "*.hs" | xargs ormolu -i diff --git a/secp256k1-haskell-recovery/CHANGELOG.md b/secp256k1-haskell-recovery/CHANGELOG.md new file mode 100644 index 0000000..8f013c9 --- /dev/null +++ b/secp256k1-haskell-recovery/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## 0.1.0 +Initial version diff --git a/secp256k1-haskell-recovery/LICENSE b/secp256k1-haskell-recovery/LICENSE new file mode 100644 index 0000000..af376dd --- /dev/null +++ b/secp256k1-haskell-recovery/LICENSE @@ -0,0 +1,19 @@ +Copyright 2020 Haskoin Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/secp256k1-haskell-recovery/Setup.hs b/secp256k1-haskell-recovery/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/secp256k1-haskell-recovery/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/secp256k1-haskell-recovery/package.yaml b/secp256k1-haskell-recovery/package.yaml new file mode 100644 index 0000000..2165547 --- /dev/null +++ b/secp256k1-haskell-recovery/package.yaml @@ -0,0 +1,44 @@ +name: secp256k1-haskell-recovery +version: 0.1.0 +synopsis: Bindings for recoverable signatures feature of secp256k1 +description: Sign and verify recoverable signatures using the secp256k1 library. +category: Crypto +author: Evgeny Osipenko +maintainer: jprupp@protonmail.ch +copyright: (c) 2023 Evgeny Osipenko +license: MIT +license-file: LICENSE +github: haskoin/secp256k1-haskell +homepage: http://github.com/haskoin/secp256k1-haskell#readme +extra-source-files: + - CHANGELOG.md +dependencies: + - base >=4.9 && <5 + - base16 >=1.0 + - bytestring >=0.10.8 && <0.12 + - entropy >=0.3.8 && <0.5 + - deepseq >=1.4.2 && <1.5 + - hashable >=1.2.6 && <1.5 + - QuickCheck >=2.9.2 && <2.15 + - secp256k1-haskell + - string-conversions >=0.4 && <0.5 + - unliftio-core >=0.1.0 && <0.3 +library: + source-dirs: src +tests: + spec: + main: Spec.hs + source-dirs: test + ghc-options: + - -threaded + - -rtsopts + - -with-rtsopts=-N + verbatim: + build-tool-depends: + hspec-discover:hspec-discover + dependencies: + - hspec + - secp256k1-haskell-recovery + - monad-par + - mtl + - HUnit diff --git a/secp256k1-haskell-recovery/secp256k1-haskell-recovery.cabal b/secp256k1-haskell-recovery/secp256k1-haskell-recovery.cabal new file mode 100644 index 0000000..715e054 --- /dev/null +++ b/secp256k1-haskell-recovery/secp256k1-haskell-recovery.cabal @@ -0,0 +1,75 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.36.0. +-- +-- see: https://github.com/sol/hpack + +name: secp256k1-haskell-recovery +version: 0.1.0 +synopsis: Bindings for recoverable signatures feature of secp256k1 +description: Sign and verify recoverable signatures using the secp256k1 library. +category: Crypto +homepage: http://github.com/haskoin/secp256k1-haskell#readme +bug-reports: https://github.com/haskoin/secp256k1-haskell/issues +author: Evgeny Osipenko +maintainer: jprupp@protonmail.ch +copyright: (c) 2023 Evgeny Osipenko +license: MIT +license-file: LICENSE +build-type: Simple +extra-source-files: + CHANGELOG.md + +source-repository head + type: git + location: https://github.com/haskoin/secp256k1-haskell + +library + exposed-modules: + Crypto.Secp256k1.Internal.Recovery + Crypto.Secp256k1.Internal.RecoveryOps + Crypto.Secp256k1.Recovery + other-modules: + Paths_secp256k1_haskell_recovery + hs-source-dirs: + src + build-depends: + QuickCheck >=2.9.2 && <2.15 + , base >=4.9 && <5 + , base16 >=1.0 + , bytestring >=0.10.8 && <0.12 + , deepseq >=1.4.2 && <1.5 + , entropy >=0.3.8 && <0.5 + , hashable >=1.2.6 && <1.5 + , secp256k1-haskell + , string-conversions ==0.4.* + , unliftio-core >=0.1.0 && <0.3 + default-language: Haskell2010 + +test-suite spec + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: + Crypto.Secp256k1.RecoverySpec + Paths_secp256k1_haskell_recovery + hs-source-dirs: + test + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: + HUnit + , QuickCheck >=2.9.2 && <2.15 + , base >=4.9 && <5 + , base16 >=1.0 + , bytestring >=0.10.8 && <0.12 + , deepseq >=1.4.2 && <1.5 + , entropy >=0.3.8 && <0.5 + , hashable >=1.2.6 && <1.5 + , hspec + , monad-par + , mtl + , secp256k1-haskell + , secp256k1-haskell-recovery + , string-conversions ==0.4.* + , unliftio-core >=0.1.0 && <0.3 + default-language: Haskell2010 + build-tool-depends: hspec-discover:hspec-discover diff --git a/secp256k1-haskell-recovery/src/Crypto/Secp256k1/Internal/Recovery.hs b/secp256k1-haskell-recovery/src/Crypto/Secp256k1/Internal/Recovery.hs new file mode 100644 index 0000000..9bdf3df --- /dev/null +++ b/secp256k1-haskell-recovery/src/Crypto/Secp256k1/Internal/Recovery.hs @@ -0,0 +1,179 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE NoFieldSelectors #-} + +-- | +-- Module : Crypto.Secp256k1.Internal.Recovery +-- License : UNLICENSE +-- Maintainer : Jean-Pierre Rupp +-- Stability : experimental +-- Portability : POSIX +-- +-- Crytpographic functions related to recoverable signatures from Bitcoin’s secp256k1 library. +-- +-- The API for this module may change at any time. This is an internal module only +-- exposed for hacking and experimentation. +module Crypto.Secp256k1.Internal.Recovery where + +import Control.DeepSeq (NFData) +import Control.Monad (unless, (<=<)) +import Crypto.Secp256k1.Internal.Base (Msg (..), PubKey (..), SecKey (..), Sig (..)) +import Crypto.Secp256k1.Internal.Context (Ctx (..)) +import Crypto.Secp256k1.Internal.ForeignTypes + ( isSuccess, + ) +import Crypto.Secp256k1.Internal.RecoveryOps + ( ecdsaRecover, + ecdsaRecoverableSignatureConvert, + ecdsaRecoverableSignatureParseCompact, + ecdsaRecoverableSignatureSerializeCompact, + ecdsaSignRecoverable, + ) +import Crypto.Secp256k1.Internal.Util + ( decodeHex, + showsHex, + unsafePackByteString, + unsafeUseByteString, + ) +import Data.ByteString (ByteString) +import Data.ByteString qualified as BS +import Data.Maybe (fromMaybe) +import Data.String (IsString (..)) +import Data.Word (Word8) +import Foreign + ( alloca, + free, + mallocBytes, + nullFunPtr, + nullPtr, + peek, + ) +import GHC.Generics (Generic) +import System.IO.Unsafe (unsafePerformIO) +import Text.Read + ( Lexeme (String), + lexP, + parens, + pfail, + readPrec, + ) + +newtype RecSig = RecSig {get :: ByteString} + deriving (Eq, Generic, NFData) + +data CompactRecSig = CompactRecSig + { rs :: !ByteString, + v :: {-# UNPACK #-} !Word8 + } + deriving (Eq, Generic) + +instance NFData CompactRecSig + +compactRecSig :: ByteString -> Maybe CompactRecSig +compactRecSig bs + | BS.length bs == 65, + BS.last bs <= 3 = + Just (CompactRecSig (BS.take 64 bs) (BS.last bs)) + | otherwise = Nothing + +serializeCompactRecSig :: CompactRecSig -> ByteString +serializeCompactRecSig (CompactRecSig bs v) = + BS.snoc bs v + +compactRecSigFromString :: String -> Maybe CompactRecSig +compactRecSigFromString = compactRecSig <=< decodeHex + +instance Read CompactRecSig where + readPrec = parens $ do + String str <- lexP + maybe pfail return $ compactRecSigFromString str + +instance IsString CompactRecSig where + fromString = fromMaybe e . compactRecSigFromString + where + e = error "Could not decode signature from hex string" + +instance Show CompactRecSig where + showsPrec _ = showsHex . serializeCompactRecSig + +-- | Parse a compact ECDSA signature (64 bytes + recovery id). +importCompactRecSig :: Ctx -> CompactRecSig -> Maybe RecSig +importCompactRecSig (Ctx ctx) (CompactRecSig sig_rs sig_v) + | BS.length sig_rs == 64, + sig_v <= 3 = unsafePerformIO $ + unsafeUseByteString sig_rs $ \(sig_rs_ptr, _) -> do + out_rec_sig_ptr <- mallocBytes 65 + ret <- + ecdsaRecoverableSignatureParseCompact + ctx + out_rec_sig_ptr + sig_rs_ptr + (fromIntegral sig_v) + if isSuccess ret + then do + out_bs <- unsafePackByteString (out_rec_sig_ptr, 65) + return (Just (RecSig out_bs)) + else do + free out_rec_sig_ptr + return Nothing + | otherwise = Nothing + +-- | Serialize an ECDSA signature in compact format (64 bytes + recovery id). +exportCompactRecSig :: Ctx -> RecSig -> CompactRecSig +exportCompactRecSig (Ctx ctx) (RecSig rec_sig_bs) = unsafePerformIO $ + unsafeUseByteString rec_sig_bs $ \(rec_sig_ptr, _) -> + alloca $ \out_v_ptr -> do + out_sig_ptr <- mallocBytes 64 + ret <- + ecdsaRecoverableSignatureSerializeCompact + ctx + out_sig_ptr + out_v_ptr + rec_sig_ptr + unless (isSuccess ret) $ do + free out_sig_ptr + error "Could not obtain compact signature" + out_bs <- unsafePackByteString (out_sig_ptr, 64) + out_v <- peek out_v_ptr + return $ CompactRecSig out_bs (fromIntegral out_v) + +-- | Convert a recoverable signature into a normal signature. +convertRecSig :: Ctx -> RecSig -> Sig +convertRecSig (Ctx ctx) (RecSig rec_sig_bs) = unsafePerformIO $ + unsafeUseByteString rec_sig_bs $ \(rec_sig_ptr, _) -> do + out_ptr <- mallocBytes 64 + ret <- ecdsaRecoverableSignatureConvert ctx out_ptr rec_sig_ptr + unless (isSuccess ret) $ + error "Could not convert a recoverable signature" + out_bs <- unsafePackByteString (out_ptr, 64) + return $ Sig out_bs + +-- | Create a recoverable ECDSA signature. +signRecMsg :: Ctx -> SecKey -> Msg -> RecSig +signRecMsg (Ctx ctx) (SecKey sec_key) (Msg m) = unsafePerformIO $ + unsafeUseByteString sec_key $ \(sec_key_ptr, _) -> + unsafeUseByteString m $ \(msg_ptr, _) -> do + rec_sig_ptr <- mallocBytes 65 + ret <- ecdsaSignRecoverable ctx rec_sig_ptr msg_ptr sec_key_ptr nullFunPtr nullPtr + unless (isSuccess ret) $ do + free rec_sig_ptr + error "could not sign message" + RecSig <$> unsafePackByteString (rec_sig_ptr, 65) + +-- | Recover an ECDSA public key from a signature. +recover :: Ctx -> RecSig -> Msg -> Maybe PubKey +recover (Ctx ctx) (RecSig rec_sig) (Msg m) = unsafePerformIO $ + unsafeUseByteString rec_sig $ \(rec_sig_ptr, _) -> + unsafeUseByteString m $ \(msg_ptr, _) -> do + pub_key_ptr <- mallocBytes 64 + ret <- ecdsaRecover ctx pub_key_ptr rec_sig_ptr msg_ptr + if isSuccess ret + then do + pub_key_bs <- unsafePackByteString (pub_key_ptr, 64) + return (Just (PubKey pub_key_bs)) + else do + free pub_key_ptr + return Nothing diff --git a/secp256k1-haskell-recovery/src/Crypto/Secp256k1/Internal/RecoveryOps.hs b/secp256k1-haskell-recovery/src/Crypto/Secp256k1/Internal/RecoveryOps.hs new file mode 100644 index 0000000..73f4fa7 --- /dev/null +++ b/secp256k1-haskell-recovery/src/Crypto/Secp256k1/Internal/RecoveryOps.hs @@ -0,0 +1,58 @@ +-- | +-- Module : Crypto.Secp256k1.Internal.RecoveryOps +-- License : UNLICENSE +-- Maintainer : Jean-Pierre Rupp +-- Stability : experimental +-- Portability : POSIX +-- +-- The API for this module may change at any time. This is an internal module only +-- exposed for hacking and experimentation. +module Crypto.Secp256k1.Internal.RecoveryOps where + +import Crypto.Secp256k1.Internal.ForeignTypes (Compact64, LCtx, Msg32, NonceFun, PubKey64, Ret, SecKey32, Sig64) +import Foreign (FunPtr, Ptr) +import Foreign.C (CInt (..)) + +data RecSig65 + +foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_parse_compact" + ecdsaRecoverableSignatureParseCompact :: + Ptr LCtx -> + Ptr RecSig65 -> + Ptr Compact64 -> + CInt -> + IO Ret + +foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_convert" + ecdsaRecoverableSignatureConvert :: + Ptr LCtx -> + Ptr Sig64 -> + Ptr RecSig65 -> + IO Ret + +foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_serialize_compact" + ecdsaRecoverableSignatureSerializeCompact :: + Ptr LCtx -> + Ptr Compact64 -> + Ptr CInt -> + Ptr RecSig65 -> + IO Ret + +foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_sign_recoverable" + ecdsaSignRecoverable :: + Ptr LCtx -> + Ptr RecSig65 -> + Ptr Msg32 -> + Ptr SecKey32 -> + FunPtr (NonceFun a) -> + -- | nonce data + Ptr a -> + IO Ret + +foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recover" + ecdsaRecover :: + Ptr LCtx -> + Ptr PubKey64 -> + Ptr RecSig65 -> + Ptr Msg32 -> + IO Ret diff --git a/secp256k1-haskell-recovery/src/Crypto/Secp256k1/Recovery.hs b/secp256k1-haskell-recovery/src/Crypto/Secp256k1/Recovery.hs new file mode 100644 index 0000000..0e28621 --- /dev/null +++ b/secp256k1-haskell-recovery/src/Crypto/Secp256k1/Recovery.hs @@ -0,0 +1,48 @@ +-- | +-- Module : Crypto.Secp256k1.Recovery +-- License : UNLICENSE +-- Maintainer : Jean-Pierre Rupp +-- Stability : experimental +-- Portability : POSIX +-- +-- Crytpographic functions related to recoverable signatures from Bitcoin’s secp256k1 library. +module Crypto.Secp256k1.Recovery + ( -- * Context + Ctx (..), + withContext, + randomizeContext, + createContext, + cloneContext, + destroyContext, + + -- * Recovery + RecSig (..), + CompactRecSig (..), + compactRecSig, + serializeCompactRecSig, + importCompactRecSig, + exportCompactRecSig, + convertRecSig, + signRecMsg, + recover, + + -- * Messages + Msg (..), + msg, + + -- * Secret Keys + SecKey (..), + secKey, + derivePubKey, + + -- * Public Keys + PubKey (..), + pubKey, + importPubKey, + exportPubKey, + ) +where + +import Crypto.Secp256k1.Internal.Base +import Crypto.Secp256k1.Internal.Context +import Crypto.Secp256k1.Internal.Recovery diff --git a/secp256k1-haskell-recovery/test/Crypto/Secp256k1/RecoverySpec.hs b/secp256k1-haskell-recovery/test/Crypto/Secp256k1/RecoverySpec.hs new file mode 100644 index 0000000..d757591 --- /dev/null +++ b/secp256k1-haskell-recovery/test/Crypto/Secp256k1/RecoverySpec.hs @@ -0,0 +1,113 @@ +module Crypto.Secp256k1.RecoverySpec (spec) where + +import Crypto.Secp256k1 (verifySig) +import Crypto.Secp256k1.Recovery +import Data.Base16.Types (assertBase16, extractBase16) +import qualified Data.ByteString as BS +import Data.ByteString.Base16 (decodeBase16, encodeBase16) +import qualified Data.ByteString.Char8 as B8 +import Data.Maybe (fromJust) +import Data.String (fromString) +import Data.String.Conversions (cs) +import Test.HUnit (Assertion, assertEqual) +import Test.Hspec +import Test.QuickCheck + +spec :: Spec +spec = around withContext $ do + describe "recovery" $ do + it "recovers public keys" $ property . recoverTest + it "recovers key from signed message" $ property . signRecMsgTest + it "does not recover bad public keys" $ property . badRecoverTest + it "detects bad recoverable signature" $ property . badRecSignatureTest + it "serializes compact recoverable signature" $ property . serializeCompactRecSigTest + it "shows and reads recoverable signature" $ property . showReadCompactRecSig + it "reads recoverable signature from string" $ property . isStringCompactRecSig + it "produces the expected signature" $ property . producesExpectedSignature + it "recovers the expected pub key" $ property . recoversExpectedPubKey + +hexToBytes :: String -> BS.ByteString +hexToBytes = decodeBase16 . assertBase16 . B8.pack + +bytesToHex :: B8.ByteString -> String +bytesToHex = cs . extractBase16 . encodeBase16 + +showRead :: (Show a, Read a, Eq a) => a -> Bool +showRead x = read (show x) == x + +exampleSecKey :: SecKey +exampleSecKey = + fromJust . secKey $ + hexToBytes "0101010101010101010101010101010101010101010101010101010101010101" + +examplePubKey' :: Ctx -> PubKey +examplePubKey' ctx = + derivePubKey ctx exampleSecKey + +exampleMsg :: Msg +exampleMsg = + fromJust . msg $ + hexToBytes "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + +exampleRecSig' :: Ctx -> RecSig +exampleRecSig' ctx = + fromJust . importCompactRecSig ctx . fromJust . compactRecSig $ + hexToBytes + "02559ab98a8908ba4cf0f914eb8b66651405ab69ab7c461dd140e40baa1b5e1d\ + \2ef6ac88a13e2226f76a9d8d49bb9cf3061dac1364c0dfe7b69cd165a8d08f07\ + \00" + +recoverTest :: Ctx -> (Msg, SecKey) -> Bool +recoverTest ctx (fm, fk) = recover ctx fg fm == Just fp + where + fp = derivePubKey ctx fk + fg = signRecMsg ctx fk fm + +signRecMsgTest :: Ctx -> (Msg, SecKey) -> Bool +signRecMsgTest ctx (fm, fk) = verifySig ctx fp fg fm + where + fp = derivePubKey ctx fk + fg = convertRecSig ctx $ signRecMsg ctx fk fm + +badRecoverTest :: Ctx -> (Msg, SecKey, Msg) -> Property +badRecoverTest ctx (fm, fk, fm') = + fm' /= fm ==> fp' /= Nothing ==> fp' /= Just fp + where + fg = signRecMsg ctx fk fm + fp = derivePubKey ctx fk + fp' = recover ctx fg fm' + +badRecSignatureTest :: Ctx -> (Msg, SecKey, SecKey) -> Bool +badRecSignatureTest ctx (fm, fk, fk2) = not $ verifySig ctx fp fg fm + where + fp = derivePubKey ctx fk2 + fg = convertRecSig ctx $ signRecMsg ctx fk fm + +serializeCompactRecSigTest :: Ctx -> (Msg, SecKey) -> Bool +serializeCompactRecSigTest ctx (fm, fk) = + case importCompactRecSig ctx $ exportCompactRecSig ctx fg of + Just fg' -> fg == fg' + Nothing -> False + where + fg = signRecMsg ctx fk fm + +showReadCompactRecSig :: Ctx -> (SecKey, Msg) -> Bool +showReadCompactRecSig ctx (k, m) = showRead crecSig + where + crecSig = exportCompactRecSig ctx $ signRecMsg ctx k m + +isStringCompactRecSig :: Ctx -> (SecKey, Msg) -> Bool +isStringCompactRecSig ctx (k, m) = Just g == importCompactRecSig ctx (fromString hex) + where + g = signRecMsg ctx k m + hex = bytesToHex . serializeCompactRecSig $ exportCompactRecSig ctx g + +producesExpectedSignature :: Ctx -> Assertion +producesExpectedSignature ctx = + assertEqual "produced signature matches" (exportCompactRecSig ctx (exampleRecSig' ctx)) $ + exportCompactRecSig ctx (signRecMsg ctx exampleSecKey exampleMsg) + +recoversExpectedPubKey :: Ctx -> Assertion +recoversExpectedPubKey ctx = + assertEqual "recovered pub key matches" (Just (examplePubKey' ctx)) $ + recover ctx (exampleRecSig' ctx) exampleMsg diff --git a/secp256k1-haskell-recovery/test/Spec.hs b/secp256k1-haskell-recovery/test/Spec.hs new file mode 100644 index 0000000..a824f8c --- /dev/null +++ b/secp256k1-haskell-recovery/test/Spec.hs @@ -0,0 +1 @@ +{-# OPTIONS_GHC -F -pgmF hspec-discover #-} diff --git a/stack.yaml b/stack.yaml index b5596d2..fc79b78 100644 --- a/stack.yaml +++ b/stack.yaml @@ -2,6 +2,7 @@ resolver: lts-21.4 packages: - secp256k1-haskell +- secp256k1-haskell-recovery nix: packages: