From 9e5e5f45f3e5c0ea9c5fef2d1b8089095cde5b29 Mon Sep 17 00:00:00 2001 From: Gabriella Gonzalez Date: Wed, 15 Jun 2022 15:40:13 -0700 Subject: [PATCH] Add support for signatures --- cbits/nix.cpp | 12 +++++++++ nix-serve-ng.cabal | 2 ++ nix/nix-serve-ng.nix | 12 +++++---- src/Main.hs | 41 ++++++++++++++++++++++++++--- src/Nix.hsc | 61 ++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 114 insertions(+), 14 deletions(-) diff --git a/cbits/nix.cpp b/cbits/nix.cpp index 5a4ed0c..b6231a5 100644 --- a/cbits/nix.cpp +++ b/cbits/nix.cpp @@ -146,4 +146,16 @@ void freePathInfo(struct PathInfo * const input) freeStrings(&input->sigs); } +// TODO: This can be done in Haskell using the `ed25519` package +void signString + ( char const * const secretKey + , char const * const message + , struct string * const output + ) +{ + std::string signature = SecretKey(secretKey).signDetached(message); + + copyString(signature, output); +} + } diff --git a/nix-serve-ng.cabal b/nix-serve-ng.cabal index 5218249..282816f 100644 --- a/nix-serve-ng.cabal +++ b/nix-serve-ng.cabal @@ -47,6 +47,8 @@ executable nix-serve-ng cxx-options: -std=c++17 build-depends: base < 5 + , base16 + , base32 , bsd-sysctl , bytestring , http-types diff --git a/nix/nix-serve-ng.nix b/nix/nix-serve-ng.nix index 6a868c9..09591d0 100644 --- a/nix/nix-serve-ng.nix +++ b/nix/nix-serve-ng.nix @@ -1,6 +1,7 @@ -{ mkDerivation, base, bsd-sysctl, bytestring, http-types, lib -, managed, megaparsec, mtl, network, nixstore, nixutil -, optparse-applicative, vector, wai, wai-extra, warp, warp-tls +{ mkDerivation, base, base16, base32, bsd-sysctl, bytestring +, http-types, lib, managed, megaparsec, mtl, network, nixstore +, nixutil, optparse-applicative, vector, wai, wai-extra, warp +, warp-tls }: mkDerivation { pname = "nix-serve-ng"; @@ -9,8 +10,9 @@ mkDerivation { isLibrary = false; isExecutable = true; executableHaskellDepends = [ - base bsd-sysctl bytestring http-types managed megaparsec mtl - network optparse-applicative vector wai wai-extra warp warp-tls + base base16 base32 bsd-sysctl bytestring http-types managed + megaparsec mtl network optparse-applicative vector wai wai-extra + warp warp-tls ]; executableSystemDepends = [ nixstore nixutil ]; description = "A drop-in replacement for nix-serve that's faster and more stable"; diff --git a/src/Main.hs b/src/Main.hs index 6357915..5cea129 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -18,6 +18,7 @@ import Sysctl (_SO_MAX_CONN) import qualified Control.Monad as Monad import qualified Control.Monad.Except as Except import qualified Data.ByteString as ByteString +import qualified Data.ByteString.Char8 as ByteString.Char8 import qualified Data.ByteString.Builder as Builder import qualified Data.ByteString.Lazy as ByteString.Lazy import qualified Data.Vector as Vector @@ -32,9 +33,16 @@ import qualified Nix import qualified Options import qualified Options.Applicative as Options import qualified System.BSD.Sysctl as Sysctl +import qualified System.Environment as Environment -makeApplication :: Integer -> ByteString -> Application -makeApplication priority storeDirectory request respond = do +data ApplicationOptions = ApplicationOptions + { priority :: Integer + , storeDirectory :: ByteString + , secretKey :: Maybe ByteString + } + +makeApplication :: ApplicationOptions -> Application +makeApplication ApplicationOptions{..} request respond = do let stripStore = ByteString.stripPrefix (storeDirectory <> "/") let done = Except.throwError @@ -76,7 +84,7 @@ makeApplication priority storeDirectory request respond = do Just storePath -> do return storePath - PathInfo{..} <- liftIO (Nix.queryPathInfo storePath) + pathInfo@PathInfo{..} <- liftIO (Nix.queryPathInfo storePath) narHash2 <- case ByteString.stripPrefix "sha256:" narHash of Nothing -> do @@ -115,6 +123,23 @@ makeApplication priority storeDirectory request respond = do Nothing -> return mempty + fingerprint <- case Nix.fingerprintPath storePath pathInfo of + Nothing -> internalError "invalid NAR hash" + Just builder -> do + return (ByteString.Lazy.toStrict (Builder.toLazyByteString builder)) + + signatures <- case secretKey of + Just key -> do + signature <- liftIO (Nix.signString key fingerprint) + + return (Vector.singleton signature) + + Nothing -> do + return sigs + + let buildSignature signature = + "Sig: " <> Builder.byteString signature <> "\n" + let builder = "StorePath: " <> Builder.byteString storePath @@ -129,6 +154,7 @@ makeApplication priority storeDirectory request respond = do <> "\n" <> referencesBuilder <> deriverBuilder + <> foldMap buildSignature signatures let size = ( ByteString.Lazy.toStrict @@ -197,6 +223,9 @@ toSocket path = do return socket +readSecretKey :: FilePath -> IO ByteString +readSecretKey = fmap ByteString.Char8.strip . ByteString.readFile + main :: IO () main = do options@Options{ priority, verbosity } <- do @@ -204,13 +233,17 @@ main = do storeDirectory <- Nix.getStoreDir + secretKeyFile <- Environment.lookupEnv "NIX_SECRET_KEY_FILE" + + secretKey <- traverse readSecretKey secretKeyFile + let logger = case verbosity of Quiet -> id Normal -> RequestLogger.logStdout Verbose -> RequestLogger.logStdoutDev - let application = logger (makeApplication priority storeDirectory) + let application = logger (makeApplication ApplicationOptions{..}) case options of Options{ ssl = Disabled, socket = TCP{ host, port } } -> do diff --git a/src/Nix.hsc b/src/Nix.hsc index af06aee..309e58a 100644 --- a/src/Nix.hsc +++ b/src/Nix.hsc @@ -1,23 +1,30 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE ForeignFunctionInterface #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} module Nix where +import Control.Applicative (empty) import Control.Monad.Managed (Managed) import Data.ByteString (ByteString) +import Data.ByteString.Builder (Builder) import Data.Vector (Vector) import Data.Word (Word64) import Foreign (Ptr, Storable(..)) import Foreign.C (CChar, CLong, CSize, CString) -import qualified Control.Exception as Exception -import qualified Control.Monad.Managed as Managed -import qualified Data.ByteString as ByteString -import qualified Data.Vector as Vector -import qualified Data.Vector.Storable as Vector.Storable +import qualified Control.Exception as Exception +import qualified Control.Monad.Managed as Managed +import qualified Data.ByteString as ByteString +import qualified Data.ByteString.Base16 as Base16 +import qualified Data.ByteString.Base32 as Base32 +import qualified Data.ByteString.Builder as Builder +import qualified Data.Vector as Vector +import qualified Data.Vector.Storable as Vector.Storable import qualified Foreign #include "nix.hh" @@ -208,4 +215,48 @@ queryPathInfo storePath = do cPathInfo <- peek output fromCPathInfo cPathInfo +fingerprintPath :: ByteString -> PathInfo -> Maybe Builder +fingerprintPath storePath PathInfo{ narHash, narSize, references } = do + suffix <- ByteString.stripPrefix "sha256:" narHash + base32Suffix <- if + | ByteString.length suffix == 64 + , Right digest <- Base16.decodeBase16 suffix -> + return (Base32.encodeBase32' digest) + | ByteString.length suffix == 52 -> + return suffix + | otherwise -> + empty + + return + ( "1;" + <> Builder.byteString storePath + <> ";sha256:" + <> Builder.byteString base32Suffix + <> ";" + <> Builder.word64Dec narSize + <> ";" + <> referencesBuilder + ) + where + referencesBuilder = + case Vector.uncons references of + Nothing -> + mempty + Just (r0, rs) -> + Builder.byteString r0 + <> foldMap (\r -> "," <> Builder.byteString r) rs + +foreign import ccall "signString" signString_ + :: CString -> CString -> Ptr String_ -> IO () + +signString :: ByteString -> ByteString -> IO ByteString +signString secretKey fingerprint = + ByteString.useAsCString secretKey \cSecretKey -> + ByteString.useAsCString fingerprint \cFingerprint -> + Foreign.alloca \output -> do + let open = signString_ cSecretKey cFingerprint output + let close = freeString output + Exception.bracket_ open close do + string_ <- peek output + fromString_ string_