Merge pull request #264 from squalus/fingerprint

core: add fingerprints
This commit is contained in:
Richard Marko 2023-11-27 14:12:00 +01:00 committed by GitHub
commit d8e337513f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 120 additions and 0 deletions

View File

@ -62,6 +62,7 @@ library
, System.Nix.ContentAddress
, System.Nix.Derivation
, System.Nix.DerivedPath
, System.Nix.Fingerprint
, System.Nix.Hash
, System.Nix.Hash.Truncation
, System.Nix.Signature
@ -98,6 +99,7 @@ test-suite core
main-is: Driver.hs
other-modules:
Derivation
Fingerprint
Hash
Signature
hs-source-dirs:
@ -111,6 +113,7 @@ test-suite core
, base16-bytestring
, base64-bytestring
, bytestring
, containers
, crypton
, data-default-class
, hspec
@ -118,3 +121,5 @@ test-suite core
, tasty-golden
, tasty-hspec
, text
, time
, unordered-containers

View File

@ -0,0 +1,46 @@
{-# LANGUAGE OverloadedStrings #-}
{-|
Description : Fingerprint of Nix store path metadata used for signature verification
-}
module System.Nix.Fingerprint
( fingerprint
, metadataFingerprint
) where
import Crypto.Hash (Digest)
import Data.Dependent.Sum (DSum)
import Data.List (sort)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text.Lazy (toStrict)
import Data.Text.Lazy.Builder (toLazyText)
import Data.Word (Word64)
import System.Nix.Hash (HashAlgo, algoDigestBuilder)
import System.Nix.StorePath
import System.Nix.StorePath.Metadata (Metadata(..))
import qualified Data.HashSet as HashSet
import qualified Data.Text as Text
-- | Produce the message signed by a NAR signature
metadataFingerprint :: StoreDir -> StorePath -> Metadata StorePath -> Text
metadataFingerprint storeDir storePath Metadata{..} = let
narSize = fromMaybe 0 narBytes
in fingerprint storeDir storePath narHash narSize (HashSet.toList references)
-- | Produce the message signed by a NAR signature
fingerprint :: StoreDir
-> StorePath
-> DSum HashAlgo Digest -- ^ NAR hash
-> Word64 -- ^ NAR size, in bytes
-> [StorePath] -- ^ References
-> Text
fingerprint storeDir storePath narHash narSize refs = let
encodedStorePath = storePathToText storeDir storePath
encodedNarHash = (toStrict . toLazyText . algoDigestBuilder) narHash
encodedNarSize = (Text.pack . show) narSize
sortedRefs = sort (storePathToText storeDir <$> refs)
encodedRefs = Text.intercalate "," sortedRefs
in Text.intercalate ";" [ "1", encodedStorePath, encodedNarHash, encodedNarSize, encodedRefs]

View File

@ -0,0 +1,69 @@
{-# LANGUAGE OverloadedStrings #-}
-- Test case from https://code.tvl.fyi/commit/tvix/nix-compat/src/narinfo/fingerprint.rs?id=a834966efd64c1b2306241c3ef20f4258f6b9c4e
module Fingerprint where
import Crypto.Error (CryptoFailable(..))
import Data.Default.Class
import System.Nix.Base (decodeWith, BaseEncoding(Base64))
import System.Nix.Fingerprint
import System.Nix.Signature
import System.Nix.StorePath
import System.Nix.StorePath.Metadata
import System.Nix.Hash (mkNamedDigest)
import Data.Text (Text)
import Data.Time.Clock (UTCTime(..))
import Data.Time.Calendar.OrdinalDate (fromOrdinalDate)
import Test.Hspec
import qualified Crypto.PubKey.Ed25519 as Ed25519
import qualified Data.HashSet as HashSet
import qualified Data.Set as Set
import qualified Data.Text.Encoding as Text
spec_fingerprint :: Spec
spec_fingerprint = do
describe "fingerprint" $ do
it "is valid for example metadata" $
metadataFingerprint def exampleStorePath exampleMetadata `shouldBe` exampleFingerprint
it "allows a successful signature verification" $ do
let msg = Text.encodeUtf8 $ metadataFingerprint def exampleStorePath exampleMetadata
Signature sig' = head $ sig <$> filter (\(NarSignature publicKey _) -> publicKey == "cache.nixos.org-1") (Set.toList (sigs exampleMetadata))
sig' `shouldSatisfy` Ed25519.verify pubkey msg
exampleFingerprint :: Text
exampleFingerprint = "1;/nix/store/syd87l2rxw8cbsxmxl853h0r6pdwhwjr-curl-7.82.0-bin;sha256:1b4sb93wp679q4zx9k1ignby1yna3z7c4c2ri3wphylbc2dwsys0;196040;/nix/store/0jqd0rlxzra1rs38rdxl43yh6rxchgc6-curl-7.82.0,/nix/store/6w8g7njm4mck5dmjxws0z1xnrxvl81xa-glibc-2.34-115,/nix/store/j5jxw3iy7bbz4a57fh9g2xm2gxmyal8h-zlib-1.2.12,/nix/store/yxvjs9drzsphm9pcf42a4byzj1kb9m7k-openssl-1.1.1n";
exampleStorePath :: StorePath
exampleStorePath = forceRight $ parsePath def "/nix/store/syd87l2rxw8cbsxmxl853h0r6pdwhwjr-curl-7.82.0-bin"
exampleMetadata :: Metadata StorePath
exampleMetadata = Metadata
{ deriverPath = Just $ forceRight $ parsePath def "/nix/store/5rwxzi7pal3qhpsyfc16gzkh939q1np6-curl-7.82.0.drv"
, narHash = forceRight $ mkNamedDigest "sha256" "1b4sb93wp679q4zx9k1ignby1yna3z7c4c2ri3wphylbc2dwsys0"
, references = HashSet.fromList $ forceRight . parsePath def <$> ["/nix/store/0jqd0rlxzra1rs38rdxl43yh6rxchgc6-curl-7.82.0","/nix/store/6w8g7njm4mck5dmjxws0z1xnrxvl81xa-glibc-2.34-115","/nix/store/j5jxw3iy7bbz4a57fh9g2xm2gxmyal8h-zlib-1.2.12","/nix/store/yxvjs9drzsphm9pcf42a4byzj1kb9m7k-openssl-1.1.1n"]
, registrationTime = UTCTime (fromOrdinalDate 0 0) 0
, narBytes = Just 196040
, trust = BuiltElsewhere
, sigs = Set.fromList $ forceRight . parseSignature <$> ["cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==", "test1:519iiVLx/c4Rdt5DNt6Y2Jm6hcWE9+XY69ygiWSZCNGVcmOcyL64uVAJ3cV8vaTusIZdbTnYo9Y7vDNeTmmMBQ=="]
, contentAddress = Nothing
}
pubkey :: Ed25519.PublicKey
pubkey = forceDecodeB64Pubkey "6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
forceDecodeB64Pubkey :: Text -> Ed25519.PublicKey
forceDecodeB64Pubkey b64EncodedPubkey = let
decoded = forceRight $ decodeWith Base64 b64EncodedPubkey
in case Ed25519.publicKey decoded of
CryptoFailed err -> (error . show) err
CryptoPassed x -> x
forceRight :: Either a b -> b
forceRight = \case
Right x -> x
_ -> error "fromRight failed"